Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintignore4
-rw-r--r--.eslintrc12
-rw-r--r--.flayignore1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml124
-rw-r--r--.gitlab/issue_templates/Bug.md24
-rw-r--r--.gitlab/issue_templates/Research Proposal.md17
-rw-r--r--.rubocop.yml301
-rw-r--r--.rubocop_todo.yml337
-rw-r--r--CHANGELOG.md274
-rw-r--r--CONTRIBUTING.md145
-rw-r--r--GITALY_SERVER_VERSION1
-rw-r--r--GITLAB_PAGES_VERSION1
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile142
-rw-r--r--Gemfile.lock234
-rw-r--r--LICENSE2
-rw-r--r--PROCESS.md64
-rw-r--r--README.md2
-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
-rwxr-xr-xapp/assets/images/favicon-blue.icobin0 -> 5430 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.js121
-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.js256
-rw-r--r--app/assets/javascripts/aside.js45
-rw-r--r--app/assets/javascripts/autosave.js109
-rw-r--r--app/assets/javascripts/awards_handler.js868
-rw-r--r--app/assets/javascripts/behaviors/autosize.js4
-rw-r--r--app/assets/javascripts/behaviors/bind_in_out.js47
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js105
-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.js4
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js4
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js3
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js42
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es641
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js19
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js.es618
-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.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js4
-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/blob_edit/blob_edit_bundle.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js151
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es684
-rw-r--r--app/assets/javascripts/boards/components/board.js105
-rw-r--r--app/assets/javascripts/boards/components/board.js.es6104
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js (renamed from app/assets/javascripts/boards/components/board_blank_state.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/board_card.js69
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es679
-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.es6119
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js92
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es663
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js72
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es665
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js111
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js70
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js49
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/label.js54
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/milestone.js55
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/user.js96
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js83
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js90
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js163
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js159
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js56
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js47
-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.js59
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js7
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es66
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js14
-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.es675
-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.js175
-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.es670
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js129
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es6125
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js107
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js119
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/breakpoints.js107
-rw-r--r--app/assets/javascripts/broadcast_message.js61
-rw-r--r--app/assets/javascripts/build.js542
-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/file.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js83
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js29
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js44
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js48
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js104
-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.js67
-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.js361
-rw-r--r--app/assets/javascripts/copy_as_gfm.js.es6355
-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.es644
-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.es6125
-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.es694
-rw-r--r--app/assets/javascripts/cycle_analytics/default_event_objects.js98
-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.es6123
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js60
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es659
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js155
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js194
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6193
-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.es6113
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js26
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es626
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js62
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es663
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js68
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es648
-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.js81
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es693
-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.js443
-rw-r--r--app/assets/javascripts/dispatcher.js.es6378
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js13
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/dropzone_input.js407
-rw-r--r--app/assets/javascripts/due_date_select.js203
-rw-r--r--app/assets/javascripts/due_date_select.js.es6181
-rw-r--r--app/assets/javascripts/environments/components/environment.js196
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6223
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js71
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es649
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js21
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es622
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js516
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es6537
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js67
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es632
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js56
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es626
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js27
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es627
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js60
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js13
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es622
-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_view.js181
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js16
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es624
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js89
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es6190
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js11
-rw-r--r--app/assets/javascripts/extensions/array.js.es624
-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/dropdown_hint.js81
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es669
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js44
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es644
-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.js174
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es6126
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js17
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js123
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6112
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js189
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6207
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js379
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6230
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js (renamed from app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6)0
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js48
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es645
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js200
-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.es6380
-rw-r--r--app/assets/javascripts/gl_dropdown.js1484
-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.js16
-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/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/importer_status.js2
-rw-r--r--app/assets/javascripts/issuable.js188
-rw-r--r--app/assets/javascripts/issuable.js.es6189
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js1
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js.es61
-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.es642
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js69
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es669
-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.es6118
-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.es661
-rw-r--r--app/assets/javascripts/issuable_context.js2
-rw-r--r--app/assets/javascripts/issuable_form.js19
-rw-r--r--app/assets/javascripts/issue.js252
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-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.js118
-rw-r--r--app/assets/javascripts/label_manager.js.es6112
-rw-r--r--app/assets/javascripts/labels.js2
-rw-r--r--app/assets/javascripts/labels_select.js38
-rw-r--r--app/assets/javascripts/layout_nav.js2
-rw-r--r--app/assets/javascripts/lib/ace/ace_config_paths.js.erb25
-rw-r--r--app/assets/javascripts/lib/chart.js7
-rw-r--r--app/assets/javascripts/lib/cropper.js7
-rw-r--r--app/assets/javascripts/lib/d3.js7
-rw-r--r--app/assets/javascripts/lib/raphael.js9
-rw-r--r--app/assets/javascripts/lib/utils/animate.js2
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js112
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6113
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js342
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es6234
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js101
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.erb6
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js10
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-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.js36
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js15
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js17
-rw-r--r--app/assets/javascripts/logo.js11
-rw-r--r--app/assets/javascripts/main.js384
-rw-r--r--app/assets/javascripts/member_expiration_date.js52
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es636
-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.js92
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es692
-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.js65
-rw-r--r--app/assets/javascripts/merge_request_tabs.js358
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es6365
-rw-r--r--app/assets/javascripts/merge_request_widget.js296
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6273
-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/merged_buttons.js2
-rw-r--r--app/assets/javascripts/milestone.js163
-rw-r--r--app/assets/javascripts/milestone_select.js38
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js110
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es697
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js335
-rw-r--r--app/assets/javascripts/namespace_select.js6
-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.js31
-rw-r--r--app/assets/javascripts/network/raphael.js74
-rw-r--r--app/assets/javascripts/new_branch_form.js34
-rw-r--r--app/assets/javascripts/new_commit_form.js14
-rw-r--r--app/assets/javascripts/notes.js204
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pager.js77
-rw-r--r--app/assets/javascripts/pager.js.es673
-rw-r--r--app/assets/javascripts/pipelines.js38
-rw-r--r--app/assets/javascripts/pipelines.js.es638
-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.js100
-rw-r--r--app/assets/javascripts/profile/profile.js.es698
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js9
-rw-r--r--app/assets/javascripts/project.js36
-rw-r--r--app/assets/javascripts/project_avatar.js2
-rw-r--r--app/assets/javascripts/project_find_file.js2
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_import.js5
-rw-r--r--app/assets/javascripts/project_label_subscription.js55
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/project_new.js2
-rw-r--r--app/assets/javascripts/project_select.js2
-rw-r--r--app/assets/javascripts/project_show.js2
-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.js80
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es680
-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.js6
-rw-r--r--app/assets/javascripts/render_gfm.js4
-rw-r--r--app/assets/javascripts/render_math.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js20
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js432
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es6432
-rw-r--r--app/assets/javascripts/shortcuts.js5
-rw-r--r--app/assets/javascripts/shortcuts_blob.js44
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js9
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js6
-rw-r--r--app/assets/javascripts/shortcuts_network.js4
-rw-r--r--app/assets/javascripts/sidebar.js.es697
-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/single_file_diff.js6
-rw-r--r--app/assets/javascripts/smart_interval.js158
-rw-r--r--app/assets/javascripts/smart_interval.js.es6157
-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/star.js2
-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/subscription_select.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/task_list.js52
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js60
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es660
-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.js7
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js.es67
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js143
-rw-r--r--app/assets/javascripts/todos.js146
-rw-r--r--app/assets/javascripts/todos.js.es6165
-rw-r--r--app/assets/javascripts/tree.js8
-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/u2f/error.js2
-rw-r--r--app/assets/javascripts/u2f/register.js2
-rw-r--r--app/assets/javascripts/u2f/util.js2
-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.js158
-rw-r--r--app/assets/javascripts/user_tabs.js.es6159
-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.js10
-rw-r--r--app/assets/javascripts/users/users_bundle.js8
-rw-r--r--app/assets/javascripts/users_select.js41
-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_common_component/commit.js.es6163
-rw-r--r--app/assets/javascripts/vue_pagination/index.js.es6148
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js29
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es642
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6108
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js (renamed from app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6)0
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js87
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6131
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es6103
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js64
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js.es634
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es665
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js78
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es673
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js29
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es618
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js164
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js52
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js199
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js147
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js19
-rw-r--r--app/assets/javascripts/wikis.js69
-rw-r--r--app/assets/javascripts/wikis.js.es673
-rw-r--r--app/assets/javascripts/zen_mode.js12
-rw-r--r--app/assets/stylesheets/application.scss4
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss37
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/awards.scss10
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss56
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss173
-rw-r--r--app/assets/stylesheets/framework/emoji-sprites.scss1811
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1813
-rw-r--r--app/assets/stylesheets/framework/files.scss44
-rw-r--r--app/assets/stylesheets/framework/filters.scss188
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss144
-rw-r--r--app/assets/stylesheets/framework/header.scss110
-rw-r--r--app/assets/stylesheets/framework/highlight.scss9
-rw-r--r--app/assets/stylesheets/framework/jquery.scss68
-rw-r--r--app/assets/stylesheets/framework/layout.scss13
-rw-r--r--app/assets/stylesheets/framework/lists.scss52
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss8
-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.scss16
-rw-r--r--app/assets/stylesheets/framework/pagination.scss14
-rw-r--r--app/assets/stylesheets/framework/panels.scss8
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss200
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss25
-rw-r--r--app/assets/stylesheets/framework/variables.scss15
-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/mailers/highlighted_diff_email.scss7
-rw-r--r--app/assets/stylesheets/pages/boards.scss176
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss33
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss122
-rw-r--r--app/assets/stylesheets/pages/environments.scss255
-rw-r--r--app/assets/stylesheets/pages/events.scss6
-rw-r--r--app/assets/stylesheets/pages/groups.scss16
-rw-r--r--app/assets/stylesheets/pages/issuable.scss20
-rw-r--r--app/assets/stylesheets/pages/issues.scss11
-rw-r--r--app/assets/stylesheets/pages/labels.scss21
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss173
-rw-r--r--app/assets/stylesheets/pages/milestone.scss26
-rw-r--r--app/assets/stylesheets/pages/note_form.scss21
-rw-r--r--app/assets/stylesheets/pages/notes.scss97
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss283
-rw-r--r--app/assets/stylesheets/pages/profile.scss43
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss39
-rw-r--r--app/assets/stylesheets/pages/projects.scss56
-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.scss68
-rw-r--r--app/assets/stylesheets/pages/tree.scss36
-rw-r--r--app/assets/stylesheets/pages/wiki.scss30
-rw-r--r--app/assets/stylesheets/print.scss1
-rw-r--r--app/controllers/admin/application_settings_controller.rb6
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/background_jobs_controller.rb1
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb4
-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.rb11
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb6
-rw-r--r--app/controllers/admin/system_info_controller.rb5
-rw-r--r--app/controllers/admin/users_controller.rb20
-rw-r--r--app/controllers/application_controller.rb56
-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.rb64
-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.rb30
-rw-r--r--app/controllers/concerns/issues_action.rb3
-rw-r--r--app/controllers/concerns/merge_requests_action.rb3
-rw-r--r--app/controllers/concerns/repository_settings_redirect.rb7
-rw-r--r--app/controllers/concerns/service_params.rb5
-rw-r--r--app/controllers/concerns/snippets_actions.rb21
-rw-r--r--app/controllers/concerns/spammable_actions.rb36
-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.rb27
-rw-r--r--app/controllers/dashboard/todos_controller.rb17
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/explore/groups_controller.rb11
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb1
-rw-r--r--app/controllers/groups_controller.rb49
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb4
-rw-r--r--app/controllers/invites_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb8
-rw-r--r--app/controllers/koding_controller.rb2
-rw-r--r--app/controllers/oauth/authorizations_controller.rb44
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb9
-rw-r--r--app/controllers/profiles/keys_controller.rb9
-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/preferences_controller.rb1
-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/application_controller.rb5
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb25
-rw-r--r--app/controllers/projects/boards/issues_controller.rb15
-rw-r--r--app/controllers/projects/branches_controller.rb45
-rw-r--r--app/controllers/projects/commit_controller.rb41
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb30
-rw-r--r--app/controllers/projects/environments_controller.rb59
-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.rb63
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb118
-rw-r--r--app/controllers/projects/notes_controller.rb20
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pages_domains_controller.rb49
-rw-r--r--app/controllers/projects/pipelines_controller.rb23
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb11
-rw-r--r--app/controllers/projects/protected_branches_controller.rb32
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb14
-rw-r--r--app/controllers/projects/services_controller.rb3
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb44
-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/snippets_controller.rb34
-rw-r--r--app/controllers/projects/tags_controller.rb24
-rw-r--r--app/controllers/projects/tree_controller.rb4
-rw-r--r--app/controllers/projects/triggers_controller.rb63
-rw-r--r--app/controllers/projects/uploads_controller.rb6
-rw-r--r--app/controllers/projects/variables_controller.rb9
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb10
-rw-r--r--app/controllers/registrations_controller.rb6
-rw-r--r--app/controllers/root_controller.rb37
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb11
-rw-r--r--app/controllers/snippets_controller.rb35
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/finders/environments_finder.rb55
-rw-r--r--app/finders/group_members_finder.rb20
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb52
-rw-r--r--app/finders/issues_finder.rb8
-rw-r--r--app/finders/members_finder.rb40
-rw-r--r--app/finders/merge_requests_finder.rb8
-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/projects_finder.rb2
-rw-r--r--app/finders/todos_finder.rb4
-rw-r--r--app/helpers/application_helper.rb24
-rw-r--r--app/helpers/application_settings_helper.rb29
-rw-r--r--app/helpers/blob_helper.rb21
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/builds_helper.rb11
-rw-r--r--app/helpers/button_helper.rb4
-rw-r--r--app/helpers/ci_status_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb13
-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_routing_helper.rb10
-rw-r--r--app/helpers/issuables_helper.rb27
-rw-r--r--app/helpers/issues_helper.rb42
-rw-r--r--app/helpers/javascript_helper.rb6
-rw-r--r--app/helpers/mattermost_helper.rb6
-rw-r--r--app/helpers/merge_requests_helper.rb28
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb14
-rw-r--r--app/helpers/page_layout_helper.rb4
-rw-r--r--app/helpers/preferences_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/helpers/rss_helper.rb5
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb4
-rw-r--r--app/helpers/submodule_helper.rb10
-rw-r--r--app/helpers/tab_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb12
-rw-r--r--app/helpers/triggers_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb10
-rw-r--r--app/helpers/wiki_helper.rb13
-rw-r--r--app/mailers/emails/pipelines.rb4
-rw-r--r--app/mailers/notify.rb2
-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.rb80
-rw-r--r--app/models/award_emoji.rb8
-rw-r--r--app/models/blob.rb8
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/chat_team.rb6
-rw-r--r--app/models/ci/build.rb164
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/ci/runner.rb33
-rw-r--r--app/models/ci/runner_project.rb2
-rw-r--r--app/models/ci/stage.rb6
-rw-r--r--app/models/ci/trigger.rb15
-rw-r--r--app/models/commit.rb17
-rw-r--r--app/models/commit_status.rb25
-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.rb32
-rw-r--r--app/models/concerns/issuable.rb39
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/concerns/reactive_service.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb101
-rw-r--r--app/models/concerns/routable.rb66
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/spammable.rb18
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/concerns/uniquify.rb30
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/directly_addressed_user.rb7
-rw-r--r--app/models/environment.rb45
-rw-r--r--app/models/event.rb11
-rw-r--r--app/models/external_issue.rb7
-rw-r--r--app/models/global_milestone.rb22
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/group_milestone.rb2
-rw-r--r--app/models/guest.rb2
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb5
-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.rb65
-rw-r--r--app/models/merge_request_diff.rb5
-rw-r--r--app/models/merge_requests_closing_issues.rb8
-rw-r--r--app/models/namespace.rb68
-rw-r--r--app/models/network/graph.rb11
-rw-r--r--app/models/note.rb25
-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.rb119
-rw-r--r--app/models/personal_access_token.rb26
-rw-r--r--app/models/project.rb213
-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/chat_message/base_message.rb4
-rw-r--r--app/models/project_services/chat_message/build_message.rb28
-rw-r--r--app/models/project_services/chat_message/issue_message.rb7
-rw-r--r--app/models/project_services/chat_message/merge_message.rb4
-rw-r--r--app/models/project_services/chat_message/note_message.rb9
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb2
-rw-r--r--app/models/project_services/drone_ci_service.rb14
-rw-r--r--app/models/project_services/gitlab_ci_service.rb8
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/irker_service.rb5
-rw-r--r--app/models/project_services/issue_tracker_service.rb11
-rw-r--r--app/models/project_services/jira_service.rb10
-rw-r--r--app/models/project_services/kubernetes_service.rb36
-rw-r--r--app/models/project_services/mattermost_service.rb14
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb4
-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.rb14
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb4
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_wiki.rb11
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/repository.rb403
-rw-r--r--app/models/route.rb27
-rw-r--r--app/models/service.rb8
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/timelog.rb18
-rw-r--r--app/models/todo.rb18
-rw-r--r--app/models/upload.rb63
-rw-r--r--app/models/user.rb137
-rw-r--r--app/models/wiki_directory.rb18
-rw-r--r--app/models/wiki_page.rb51
-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/project_policy.rb51
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/policies/user_policy.rb8
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb60
-rw-r--r--app/serializers/analytics_stage_entity.rb1
-rw-r--r--app/serializers/build_entity.rb2
-rw-r--r--app/serializers/environment_entity.rb2
-rw-r--r--app/serializers/environment_serializer.rb52
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb42
-rw-r--r--app/services/access_token_validation_service.rb8
-rw-r--r--app/services/auth/container_registry_authentication_service.rb4
-rw-r--r--app/services/base_service.rb9
-rw-r--r--app/services/boards/create_service.rb1
-rw-r--r--app/services/boards/issues/list_service.rb21
-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.rb34
-rw-r--r--app/services/ci/retry_pipeline_service.rb28
-rw-r--r--app/services/ci/stop_environments_service.rb7
-rw-r--r--app/services/ci/update_runner_service.rb15
-rw-r--r--app/services/commits/change_service.rb46
-rw-r--r--app/services/compare_service.rb26
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb32
-rw-r--r--app/services/create_branch_service.rb42
-rw-r--r--app/services/create_snippet_service.rb10
-rw-r--r--app/services/create_tag_service.rb30
-rw-r--r--app/services/delete_tag_service.rb42
-rw-r--r--app/services/delete_user_service.rb31
-rw-r--r--app/services/destroy_group_service.rb29
-rw-r--r--app/services/files/base_service.rb37
-rw-r--r--app/services/files/create_dir_service.rb10
-rw-r--r--app/services/files/create_service.rb17
-rw-r--r--app/services/files/delete_service.rb7
-rw-r--r--app/services/files/destroy_service.rb15
-rw-r--r--app/services/files/multi_service.rb51
-rw-r--r--app/services/files/update_service.rb16
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_operation_service.rb156
-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.rb28
-rw-r--r--app/services/issuable_base_service.rb41
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/build_service.rb42
-rw-r--r--app/services/issues/create_service.rb32
-rw-r--r--app/services/issues/move_service.rb2
-rw-r--r--app/services/issues/update_service.rb26
-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/add_todo_when_build_fails_service.rb6
-rw-r--r--app/services/merge_requests/build_service.rb31
-rw-r--r--app/services/merge_requests/merge_service.rb46
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb24
-rw-r--r--app/services/merge_requests/refresh_service.rb12
-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/delete_service.rb7
-rw-r--r--app/services/notes/destroy_service.rb7
-rw-r--r--app/services/notes/slash_commands_service.rb2
-rw-r--r--app/services/notification_service.rb15
-rw-r--r--app/services/pages_service.rb15
-rw-r--r--app/services/projects/create_service.rb4
-rw-r--r--app/services/projects/destroy_service.rb7
-rw-r--r--app/services/projects/download_service.rb4
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/participants_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb26
-rw-r--r--app/services/projects/update_pages_configuration_service.rb69
-rw-r--r--app/services/projects/update_pages_service.rb166
-rw-r--r--app/services/projects/upload_service.rb2
-rw-r--r--app/services/protected_branches/api_update_service.rb8
-rw-r--r--app/services/slash_commands/interpret_service.rb31
-rw-r--r--app/services/spam_check_service.rb24
-rw-r--r--app/services/spam_service.rb34
-rw-r--r--app/services/system_hooks_service.rb8
-rw-r--r--app/services/system_note_service.rb43
-rw-r--r--app/services/tags/create_service.rb32
-rw-r--r--app/services/tags/destroy_service.rb46
-rw-r--r--app/services/todo_service.rb49
-rw-r--r--app/services/update_snippet_service.rb10
-rw-r--r--app/services/users/destroy_service.rb56
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb16
-rw-r--r--app/services/validate_new_branch_service.rb22
-rw-r--r--app/services/wiki_pages/destroy_service.rb11
-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.rb59
-rw-r--r--app/uploaders/gitlab_uploader.rb25
-rw-r--r--app/uploaders/records_uploads.rb34
-rw-r--r--app/uploaders/uploader_helper.rb17
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/certificate_key_validator.rb25
-rw-r--r--app/validators/certificate_validator.rb24
-rw-r--r--app/validators/duration_validator.rb17
-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.haml3
-rw-r--r--app/views/admin/application_settings/_form.html.haml53
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml8
-rw-r--r--app/views/admin/logs/show.html.haml7
-rw-r--r--app/views/admin/projects/_projects.html.haml32
-rw-r--r--app/views/admin/projects/index.html.haml94
-rw-r--r--app/views/admin/runners/_runner.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml13
-rw-r--r--app/views/admin/runners/show.html.haml8
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml2
-rw-r--r--app/views/admin/spam_logs/index.html.haml1
-rw-r--r--app/views/admin/users/_access_levels.html.haml37
-rw-r--r--app/views/admin/users/_form.html.haml23
-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/index.html.haml14
-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/ci/lints/show.html.haml2
-rw-r--r--app/views/dashboard/_activities.html.haml7
-rw-r--r--app/views/dashboard/_activity_head.html.haml15
-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.haml27
-rw-r--r--app/views/dashboard/todos/index.html.haml14
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.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/discussions/_resolve_all.html.haml3
-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.builder2
-rw-r--r--app/views/events/event/_push.html.haml8
-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.haml40
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-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/_home_panel.html.haml17
-rw-r--r--app/views/groups/_show_nav.html.haml7
-rw-r--r--app/views/groups/activity.html.haml6
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml4
-rw-r--r--app/views/groups/group_members/index.html.haml6
-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/milestones/show.html.haml4
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml50
-rw-r--r--app/views/groups/subgroups.html.haml21
-rw-r--r--app/views/help/_shortcuts.html.haml12
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/base/unauthorized.js.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/issues/_issue.atom.builder2
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml8
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml19
-rw-r--r--app/views/layouts/_recaptcha_verification.html.haml23
-rw-r--r--app/views/layouts/application.html.haml5
-rw-r--r--app/views/layouts/header/_default.html.haml29
-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.haml76
-rw-r--r--app/views/layouts/nav/_explore.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml18
-rw-r--r--app/views/layouts/nav/_project.html.haml80
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml36
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/build_fail_email.html.haml4
-rw-r--r--app/views/notify/build_fail_email.text.erb2
-rw-r--r--app/views/notify/build_success_email.html.haml4
-rw-r--r--app/views/notify/build_success_email.text.erb2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb2
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb2
-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.haml3
-rw-r--r--app/views/profiles/accounts/show.html.haml9
-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.haml69
-rw-r--r--app/views/profiles/preferences/show.html.haml13
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-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/_customize_workflow.html.haml2
-rw-r--r--app/views/projects/_head.html.haml20
-rw-r--r--app/views/projects/_home_panel.html.haml10
-rw-r--r--app/views/projects/_last_push.html.haml1
-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/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_actions.html.haml12
-rw-r--r--app/views/projects/blob/_blob.html.haml17
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml14
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml14
-rw-r--r--app/views/projects/boards/components/_board.html.haml1
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml28
-rw-r--r--app/views/projects/boards/components/_card.html.haml28
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml8
-rw-r--r--app/views/projects/builds/_header.html.haml11
-rw-r--r--app/views/projects/builds/_sidebar.html.haml12
-rw-r--r--app/views/projects/builds/_table.html.haml4
-rw-r--r--app/views/projects/builds/index.html.haml4
-rw-r--r--app/views/projects/builds/show.html.haml21
-rw-r--r--app/views/projects/buttons/_download.html.haml10
-rw-r--r--app/views/projects/ci/builds/_build.html.haml6
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml109
-rw-r--r--app/views/projects/commit/_change.html.haml12
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/commit/_pipeline.html.haml6
-rw-r--r--app/views/projects/commit/_pipelines_list.haml23
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml2
-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.haml14
-rw-r--r--app/views/projects/compare/_form.html.haml4
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml5
-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/_content.html.haml7
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml9
-rw-r--r--app/views/projects/diffs/_file_header.html.haml6
-rw-r--r--app/views/projects/diffs/_line.html.haml18
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml36
-rw-r--r--app/views/projects/diffs/_text_file.html.haml9
-rw-r--r--app/views/projects/edit.html.haml14
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml6
-rw-r--r--app/views/projects/environments/_stop.html.haml2
-rw-r--r--app/views/projects/environments/folder.html.haml14
-rw-r--r--app/views/projects/environments/index.html.haml8
-rw-r--r--app/views/projects/environments/metrics.html.haml21
-rw-r--r--app/views/projects/environments/show.html.haml7
-rw-r--r--app/views/projects/environments/terminal.html.haml4
-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/ci/_builds.haml56
-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/group_links/_index.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-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/_issue.html.haml96
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml10
-rw-r--r--app/views/projects/issues/show.html.haml14
-rw-r--r--app/views/projects/issues/verify.html.haml5
-rw-r--r--app/views/projects/labels/index.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/_merge_request.html.haml126
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml7
-rw-r--r--app/views/projects/merge_requests/_show.html.haml22
-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.haml6
-rw-r--r--app/views/projects/merge_requests/index.html.haml8
-rw-r--r--app/views/projects/merge_requests/merge.js.haml4
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml23
-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/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml22
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml2
-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/index.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml10
-rw-r--r--app/views/projects/network/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml11
-rw-r--r--app/views/projects/notes/_hints.html.haml1
-rw-r--r--app/views/projects/notes/_note.html.haml44
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/pages/_access.html.haml13
-rw-r--r--app/views/projects/pages/_destroy.haml12
-rw-r--r--app/views/projects/pages/_disabled.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml17
-rw-r--r--app/views/projects/pages/_no_domains.html.haml7
-rw-r--r--app/views/projects/pages/_use.html.haml10
-rw-r--r--app/views/projects/pages/show.html.haml28
-rw-r--r--app/views/projects/pages_domains/_form.html.haml34
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml30
-rw-r--r--app/views/projects/pipelines/_head.html.haml18
-rw-r--r--app/views/projects/pipelines/_info.html.haml8
-rw-r--r--app/views/projects/pipelines/_stage.html.haml8
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml4
-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.haml56
-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.haml53
-rw-r--r--app/views/projects/pipelines/new.html.haml6
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml7
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml96
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml98
-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/runners/_form.html.haml4
-rw-r--r--app/views/projects/runners/_index.html.haml25
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml4
-rw-r--r--app/views/projects/runners/index.html.haml27
-rw-r--r--app/views/projects/runners/show.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml30
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml17
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml32
-rw-r--r--app/views/projects/settings/_head.html.haml33
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml7
-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.haml11
-rw-r--r--app/views/projects/snippets/_actions.html.haml6
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/snippets/verify.html.haml4
-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/_readme.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.haml104
-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/triggers/index.html.haml110
-rw-r--r--app/views/projects/variables/_content.html.haml2
-rw-r--r--app/views/projects/variables/_form.html.haml2
-rw-r--r--app/views/projects/variables/_index.html.haml16
-rw-r--r--app/views/projects/variables/index.html.haml18
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml5
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml8
-rw-r--r--app/views/projects/wikis/_sidebar_wiki_page.html.haml3
-rw-r--r--app/views/projects/wikis/_wiki_directory.html.haml4
-rw-r--r--app/views/projects/wikis/_wiki_page.html.haml1
-rw-r--r--app/views/projects/wikis/pages.html.haml10
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/search/_category.html.haml26
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml57
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_branch_switcher.html.haml8
-rw-r--r--app/views/shared/_commit_message_container.html.haml4
-rw-r--r--app/views/shared/_group_form.html.haml12
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml25
-rw-r--r--app/views/shared/_label.html.haml14
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/_milestones_filter.html.haml12
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml18
-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/_visibility_level.html.haml11
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/groups/_dropdown.html.haml18
-rw-r--r--app/views/shared/groups/_group.html.haml6
-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.haml15
-rw-r--r--app/views/shared/issuable/_form.html.haml56
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml57
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml42
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml9
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml16
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml4
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml9
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml3
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml8
-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.haml26
-rw-r--r--app/views/shared/projects/_list.html.haml5
-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/_form.html.haml4
-rw-r--r--app/views/shared/web_hooks/_form.html.haml6
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml48
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml8
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/snippets/verify.html.haml4
-rw-r--r--app/views/users/calendar.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml12
-rw-r--r--app/views/users/show.html.haml20
-rw-r--r--app/workers/authorized_projects_worker.rb4
-rw-r--r--app/workers/delete_user_worker.rb4
-rw-r--r--app/workers/emails_on_push_worker.rb6
-rw-r--r--app/workers/group_destroy_worker.rb2
-rw-r--r--app/workers/irker_worker.rb6
-rw-r--r--app/workers/pages_worker.rb23
-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
-rwxr-xr-xbin/teaspoon8
-rw-r--r--changelogs/unreleased/1051-api-create-users-without-password.yml4
-rw-r--r--changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml4
-rw-r--r--changelogs/unreleased/1363-redo-mailroom-support.yml4
-rw-r--r--changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml4
-rw-r--r--changelogs/unreleased/14492-change-fork-endpoint.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/18962-update-issues-button-jumps.yml4
-rw-r--r--changelogs/unreleased/19164-mobile-settings.yml4
-rw-r--r--changelogs/unreleased/19302-wiki-page-delete-does-not-trigger-the-webhook.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/20495-plus-icon-button.yml4
-rw-r--r--changelogs/unreleased/20732_member_exists_409.yml4
-rw-r--r--changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml4
-rw-r--r--changelogs/unreleased/21240_snippets_line_ending.yml4
-rw-r--r--changelogs/unreleased/21605-allow-html5-details.yml4
-rw-r--r--changelogs/unreleased/22018-api-milestone-merge-requests.yml4
-rw-r--r--changelogs/unreleased/22132-rename-branch-name-params-to-branch.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/22645-add-discussion-contribs-to-calendar.yml4
-rw-r--r--changelogs/unreleased/22818-licence-gitignore-and-yml-endpoints-removal.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/23061-consolidate-project-lists.yml4
-rw-r--r--changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml4
-rw-r--r--changelogs/unreleased/23104-remove-public-param-for-projects.yml4
-rw-r--r--changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml4
-rw-r--r--changelogs/unreleased/23535-folders-in-wiki-repository.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/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml4
-rw-r--r--changelogs/unreleased/23948-assign-to-me.yml4
-rw-r--r--changelogs/unreleased/24137-issuable-permalink.yml4
-rw-r--r--changelogs/unreleased/24166-close-builds-dropdown.yml4
-rw-r--r--changelogs/unreleased/24333-close-issues-with-merge-request-title-ui.yml5
-rw-r--r--changelogs/unreleased/24421-personal-milestone-count-badges.yml4
-rw-r--r--changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml4
-rw-r--r--changelogs/unreleased/24501-new-file-existing-branch.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/24976-start-of-line-mention.yml4
-rw-r--r--changelogs/unreleased/24998-fix-typo-gitlab-config-file.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/25465-todo-done-clicking-is-kind-of-unsafe.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/25709-diff-file-overflow.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/26068_tasklist_issue.yml4
-rw-r--r--changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.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/26206-fix-download-dropdown.yml4
-rw-r--r--changelogs/unreleased/26286-most-recent-activity-profile-header.yml4
-rw-r--r--changelogs/unreleased/26287-link-branch-in-calendar-activity.yml4
-rw-r--r--changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml4
-rw-r--r--changelogs/unreleased/26315-unify-labels-filter-behavior.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/26379-iid-param.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/26500-informative-slack-notifications.yml4
-rw-r--r--changelogs/unreleased/26651-cannot-move-project-into-group.yml4
-rw-r--r--changelogs/unreleased/26703-todos-count.yml4
-rw-r--r--changelogs/unreleased/26705-filter-todos-by-manual-add.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/26744-add-omniauth-oauth2-generic-strategy.yml3
-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/26847-api-pipelines-use-basic.yml4
-rw-r--r--changelogs/unreleased/26852-fix-slug-for-openshift.yml4
-rw-r--r--changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml4
-rw-r--r--changelogs/unreleased/26900-pipelines-tabs.yml4
-rw-r--r--changelogs/unreleased/26908-make-timelogs-use-foreign-keys4
-rw-r--r--changelogs/unreleased/26947-build-status-self-link.yml4
-rw-r--r--changelogs/unreleased/26957-tanuki-anim-hang.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/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml4
-rw-r--r--changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.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/27178-update-builds-link-in-project-settings.yml4
-rw-r--r--changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml4
-rw-r--r--changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml4
-rw-r--r--changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml4
-rw-r--r--changelogs/unreleased/27287-label-dropdown-error-messages.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/27336-add-environment-url-link-to-terminal-page.yml4
-rw-r--r--changelogs/unreleased/27354-navigation-new-button.yml4
-rw-r--r--changelogs/unreleased/27452-update-issue-count.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/27516-fix-wrong-call-to-project_cache_worker-method.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/27608-fixes-markdown-in-activity-feed-is-light-gray.yml4
-rw-r--r--changelogs/unreleased/27610-issue-number-alignment.yml4
-rw-r--r--changelogs/unreleased/27631-fix-small-height-of-activity-header-page.yml4
-rw-r--r--changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml4
-rw-r--r--changelogs/unreleased/27762-add-default-artifacts-expiration.yml4
-rw-r--r--changelogs/unreleased/27778-a11y-sidebar.yml5
-rw-r--r--changelogs/unreleased/27783-fix-fe-doc-broken-link.yml4
-rw-r--r--changelogs/unreleased/27840-improve-search-bar-experience.yml4
-rw-r--r--changelogs/unreleased/27920-both-wip-messages-showing.yml4
-rw-r--r--changelogs/unreleased/27924-set-max-width-mini-pipeline-text.yml4
-rw-r--r--changelogs/unreleased/27934-left-align-logo.yml4
-rw-r--r--changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml4
-rw-r--r--changelogs/unreleased/27978-improve-task-list-ux.yml4
-rw-r--r--changelogs/unreleased/27994-fix-mr-widget-jump.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/28030-infinite-offset.yml4
-rw-r--r--changelogs/unreleased/28082-deleted-branch-event-404.yml4
-rw-r--r--changelogs/unreleased/28142-overlap-bugs.yml4
-rw-r--r--changelogs/unreleased/28176_merge_widget_fix.yml4
-rw-r--r--changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml4
-rw-r--r--changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml4
-rw-r--r--changelogs/unreleased/28229-pipelines-loading-icon.yml5
-rw-r--r--changelogs/unreleased/28236-browse-button-dropping.yml4
-rw-r--r--changelogs/unreleased/28247-timeloops-bug.yml4
-rw-r--r--changelogs/unreleased/28253-fix-buid-scroll-button-position.yml4
-rw-r--r--changelogs/unreleased/28257-issues-iids.yml4
-rw-r--r--changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml4
-rw-r--r--changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml4
-rw-r--r--changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml4
-rw-r--r--changelogs/unreleased/28353-little-grammar-issue.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/28389-ux-problem-with-pipeline-coverage-placeholder.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/28462-fix-delimiter-removes-issue-in-todo-counter.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/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/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/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/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/9381-authentiq-backchannel-logout.yml4
-rw-r--r--changelogs/unreleased/adam-prevent-two-issue-trackers.yml4
-rw-r--r--changelogs/unreleased/add-auto-submited-header.yml4
-rw-r--r--changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml4
-rw-r--r--changelogs/unreleased/add-filtered-search-to-mr.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-pipeline-triggers.yml4
-rw-r--r--changelogs/unreleased/add-yarn-documentation.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/alphabetically_sort_tags_on_runner_list.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-entities.yml4
-rw-r--r--changelogs/unreleased/api-notes-entity-fields.yml4
-rw-r--r--changelogs/unreleased/api-post-block.yml4
-rw-r--r--changelogs/unreleased/api-remove-deploy-key-disable.yml4
-rw-r--r--changelogs/unreleased/api-remove-owned-groups.yml4
-rw-r--r--changelogs/unreleased/api-star-restful.yml4
-rw-r--r--changelogs/unreleased/api-subscription-restful.yml4
-rw-r--r--changelogs/unreleased/api-todos-restful.yml4
-rw-r--r--changelogs/unreleased/artifactsdoc.yml4
-rw-r--r--changelogs/unreleased/backup_storage_class.yml4
-rw-r--r--changelogs/unreleased/beautiful-karma-output.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/bypass-email-domain-validation-when-created-by-admin.yml4
-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/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/copy-branch-to-clipboard.yml4
-rw-r--r--changelogs/unreleased/cover-my-karma.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-group-reference-full-name.yml4
-rw-r--r--changelogs/unreleased/document-how-to-vue.yml4
-rw-r--r--changelogs/unreleased/dynamic-header-fixture.yml4
-rw-r--r--changelogs/unreleased/dynamic-project-title-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-create-nested-groups-via-ui.yml4
-rw-r--r--changelogs/unreleased/dz-dashboard-groups-search.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-api.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/dz-refactor-full-path.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-paginated-environments-api-add-subview.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-github-find-users-by-email.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/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-cancel-integration-settings.yml4
-rw-r--r--changelogs/unreleased/fix-ci-build-policy.yml4
-rw-r--r--changelogs/unreleased/fix-cycle-analytics-events-limit.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-filtering-username-with-multiple-words.yml4
-rw-r--r--changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml4
-rw-r--r--changelogs/unreleased/fix-gb-notification-settings-when-no-repository.yml4
-rw-r--r--changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml4
-rw-r--r--changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml4
-rw-r--r--changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.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-user-validation-error.yml4
-rw-r--r--changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml4
-rw-r--r--changelogs/unreleased/fix-search-bar-search-param.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_issue_from_milestone.yml4
-rw-r--r--changelogs/unreleased/fix_updated_field_in_issues-atom.yml4
-rw-r--r--changelogs/unreleased/fixes-namespace-api-documentation.yml4
-rw-r--r--changelogs/unreleased/format-timeago-date.yml4
-rw-r--r--changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml4
-rw-r--r--changelogs/unreleased/gfm-autocomplete-fixes.yml4
-rw-r--r--changelogs/unreleased/gitaly-post-receive.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/group-memebrs-owner-level.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/instrument-in-karma.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-descrpiption-spinner-off.yml4
-rw-r--r--changelogs/unreleased/issue-newproj-layout.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_24815.yml4
-rw-r--r--changelogs/unreleased/issue_25900.yml4
-rw-r--r--changelogs/unreleased/issue_26701.yml4
-rw-r--r--changelogs/unreleased/issue_27211.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/list_issues_with_no_labels.yml4
-rw-r--r--changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml4
-rw-r--r--changelogs/unreleased/long-file-name-overflow.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/move_tags_service_to_namespace.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/new-branch-fixture.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.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/only-yield-valid-reference-matches.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/paginate-all-the-things.yml4
-rw-r--r--changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml4
-rw-r--r--changelogs/unreleased/pass_coverage_value_to_commit_status_api.yml4
-rw-r--r--changelogs/unreleased/pipeline-blocking-actions.yml4
-rw-r--r--changelogs/unreleased/priority-to-label-priority.yml4
-rw-r--r--changelogs/unreleased/protected-branch-dropdown-titles.yml4
-rw-r--r--changelogs/unreleased/quick-submit-fixture.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/removal_of_unused_parameter.yml4
-rw-r--r--changelogs/unreleased/remove-es6-extension.yml4
-rw-r--r--changelogs/unreleased/remove-inactive-default-email-services.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-datepicker.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-plugins.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-sortable.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/rename-retry-failed-pipeline-to-retry.yml4
-rw-r--r--changelogs/unreleased/rename_delete_services.yml4
-rw-r--r--changelogs/unreleased/rename_files_delete_service.yml4
-rw-r--r--changelogs/unreleased/replace-npm-with-yarn.yml4
-rw-r--r--changelogs/unreleased/requires-input-fixture.yml4
-rw-r--r--changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml4
-rw-r--r--changelogs/unreleased/rss-btn-alignment-fix.yml4
-rw-r--r--changelogs/unreleased/seed-abuse-reports.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-project-id-index-project-authorizations.yml4
-rw-r--r--changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml4
-rw-r--r--changelogs/unreleased/sh-delete-user-permission-check.yml4
-rw-r--r--changelogs/unreleased/small-screen-fullscreen-button.yml4
-rw-r--r--changelogs/unreleased/snippet-spam.yml4
-rw-r--r--changelogs/unreleased/snippets-search.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/static-navbar.yml4
-rw-r--r--changelogs/unreleased/task_list_refactor.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/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/upgrade-omniauth.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-requeue-pending-delete.yml4
-rw-r--r--changelogs/unreleased/zj-slow-service-fetch.yml4
-rw-r--r--changelogs/unreleased/zj-variables-build-job.yml4
-rw-r--r--config/application.rb40
-rw-r--r--config/dependency_decisions.yml145
-rw-r--r--config/gitlab.yml.example63
-rw-r--r--config/initializers/0_inflections.rb (renamed from config/initializers/inflections.rb)0
-rw-r--r--config/initializers/1_settings.rb106
-rw-r--r--config/initializers/4_ci_app.rb8
-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/additional_headers_interceptor.rb1
-rw-r--r--config/initializers/devise.rb19
-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/mysql_ignore_postgresql_options.rb2
-rw-r--r--config/initializers/plantuml_lexer.rb2
-rw-r--r--config/initializers/rack_lineprof.rb2
-rw-r--r--config/initializers/request_context.rb3
-rw-r--r--config/initializers/request_profiler.rb2
-rw-r--r--config/initializers/rspec_profiling.rb35
-rw-r--r--config/initializers/secret_token.rb7
-rw-r--r--config/initializers/sidekiq.rb10
-rw-r--r--config/initializers/static_files.rb31
-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.js51
-rw-r--r--config/locales/doorkeeper.en.yml1
-rw-r--r--config/mail_room.yml5
-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.rb3
-rw-r--r--config/routes/group.rb1
-rw-r--r--config/routes/profile.rb2
-rw-r--r--config/routes/project.rb28
-rw-r--r--config/routes/sidekiq.rb2
-rw-r--r--config/routes/wiki.rb2
-rw-r--r--config/sidekiq_queues.yml5
-rw-r--r--config/webpack.config.js187
-rw-r--r--db/fixtures/development/10_merge_requests.rb2
-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/18_abuse_reports.rb5
-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/20151215132013_add_pages_size_to_application_settings.rb14
-rw-r--r--db/migrate/20160210105555_create_pages_domain.rb16
-rw-r--r--db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb8
-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/20160801163709_add_submitted_as_ham_to_spam_logs.rb6
-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.rb10
-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.rb2
-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/20161202152035_add_index_to_routes.rb7
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb8
-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/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.rb54
-rw-r--r--db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb33
-rw-r--r--db/migrate/20170127032550_remove_backlog_lists_from_boards.rb17
-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/20170204172458_add_name_to_route.rb12
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb15
-rw-r--r--db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb15
-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.rb17
-rw-r--r--db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb15
-rw-r--r--db/migrate/20170210103609_add_index_to_user_agent_detail.rb18
-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/20170305203726_add_owner_id_foreign_key.rb11
-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/20170206040400_remove_inactive_default_email_services.rb41
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb23
-rw-r--r--db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb32
-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.rb13
-rw-r--r--db/post_migrate/20170214111112_delete_deprecated_gitlab_ci_service.rb15
-rw-r--r--db/post_migrate/20170215200045_remove_theme_id_from_users.rb9
-rw-r--r--db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb19
-rw-r--r--db/post_migrate/20170313133418_rename_more_reserved_project_names.rb101
-rw-r--r--db/schema.rb95
-rw-r--r--doc/README.md10
-rw-r--r--doc/administration/auth/authentiq.md2
-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/build_artifacts.md97
-rw-r--r--doc/administration/container_registry.md98
-rw-r--r--doc/administration/custom_hooks.md3
-rw-r--r--doc/administration/high_availability/database.md4
-rw-r--r--doc/administration/high_availability/load_balancer.md8
-rw-r--r--doc/administration/high_availability/nfs.md18
-rw-r--r--doc/administration/integration/plantuml.md18
-rw-r--r--doc/administration/integration/terminal.md26
-rw-r--r--doc/administration/job_artifacts.md114
-rw-r--r--doc/administration/monitoring/performance/introduction.md2
-rw-r--r--doc/administration/monitoring/performance/prometheus.md103
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/index.md147
-rw-r--r--doc/administration/monitoring/prometheus/node_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/postgres_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/redis_exporter.md33
-rw-r--r--doc/administration/pages/index.md278
-rw-r--r--doc/administration/pages/source.md396
-rw-r--r--doc/administration/reply_by_email.md60
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md2
-rw-r--r--doc/administration/repository_storage_paths.md15
-rw-r--r--doc/api/README.md30
-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.md34
-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.md46
-rw-r--r--doc/api/deploy_key_multiple_projects.md8
-rw-r--r--doc/api/deploy_keys.md53
-rw-r--r--doc/api/deployments.md4
-rw-r--r--doc/api/enviroments.md31
-rw-r--r--doc/api/groups.md125
-rw-r--r--doc/api/issues.md259
-rw-r--r--doc/api/jobs.md622
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/labels.md50
-rw-r--r--doc/api/members.md20
-rw-r--r--doc/api/merge_requests.md251
-rw-r--r--doc/api/milestones.md25
-rw-r--r--doc/api/namespaces.md13
-rw-r--r--doc/api/notes.md90
-rw-r--r--doc/api/notification_settings.md12
-rw-r--r--doc/api/oauth2.md6
-rw-r--r--doc/api/pipeline_triggers.md170
-rw-r--r--doc/api/pipelines.md54
-rw-r--r--doc/api/project_snippets.md17
-rw-r--r--doc/api/projects.md377
-rw-r--r--doc/api/repositories.md14
-rw-r--r--doc/api/repository_files.md51
-rw-r--r--doc/api/runners.md40
-rw-r--r--doc/api/services.md51
-rw-r--r--doc/api/session.md3
-rw-r--r--doc/api/settings.md34
-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.md23
-rw-r--r--doc/api/users.md119
-rw-r--r--doc/api/v3_to_v4.md82
-rw-r--r--doc/api/version.md2
-rw-r--r--doc/ci/README.md20
-rw-r--r--doc/ci/autodeploy/index.md4
-rw-r--r--doc/ci/build_artifacts/README.md5
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md96
-rw-r--r--doc/ci/docker/using_docker_images.md35
-rw-r--r--doc/ci/enable_or_disable_ci.md18
-rw-r--r--doc/ci/environments.md73
-rw-r--r--doc/ci/examples/deployment/README.md2
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md4
-rw-r--r--doc/ci/examples/php.md12
-rw-r--r--doc/ci/examples/test-scala-application.md8
-rw-r--r--doc/ci/git_submodules.md18
-rw-r--r--doc/ci/img/features_settings.pngbin9243 -> 0 bytes
-rw-r--r--doc/ci/img/permissions_settings.pngbin0 -> 39194 bytes
-rw-r--r--doc/ci/img/pipelines-goal.svg4
-rw-r--r--doc/ci/img/types-of-pipelines.svg4
-rw-r--r--doc/ci/img/view_on_env_blob.pngbin0 -> 111663 bytes
-rw-r--r--doc/ci/img/view_on_env_mr.pngbin0 -> 1005195 bytes
-rw-r--r--doc/ci/pipelines.md53
-rw-r--r--doc/ci/quick_start/README.md86
-rw-r--r--doc/ci/quick_start/img/build_log.pngbin24461 -> 35261 bytes
-rw-r--r--doc/ci/quick_start/img/builds_status.pngbin24278 -> 19127 bytes
-rw-r--r--doc/ci/quick_start/img/new_commit.pngbin4772 -> 5584 bytes
-rw-r--r--doc/ci/quick_start/img/pipelines_status.pngbin25494 -> 22872 bytes
-rw-r--r--doc/ci/quick_start/img/runners_activated.pngbin12337 -> 18215 bytes
-rw-r--r--doc/ci/quick_start/img/single_commit_status_pending.pngbin15785 -> 13631 bytes
-rw-r--r--doc/ci/quick_start/img/status_pending.pngbin9521 -> 0 bytes
-rw-r--r--doc/ci/runners/README.md132
-rw-r--r--doc/ci/services/mysql.md4
-rw-r--r--doc/ci/services/postgres.md4
-rw-r--r--doc/ci/services/redis.md2
-rw-r--r--doc/ci/ssh_keys/README.md10
-rw-r--r--doc/ci/triggers/README.md75
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin29044 -> 20383 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin8233 -> 6585 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin3652 -> 3637 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin5119 -> 5116 bytes
-rw-r--r--doc/ci/variables/README.md222
-rw-r--r--doc/ci/yaml/README.md410
-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/ci_setup.md3
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/development/doc_styleguide.md16
-rw-r--r--doc/development/frontend.md127
-rw-r--r--doc/development/gotchas.md8
-rw-r--r--doc/development/instrumentation.md2
-rw-r--r--doc/development/licensing.md7
-rw-r--r--doc/development/limit_ee_conflicts.md34
-rw-r--r--doc/development/merge_request_performance_guidelines.md4
-rw-r--r--doc/development/performance.md1
-rw-r--r--doc/development/profiling.md2
-rw-r--r--doc/development/query_recorder.md29
-rw-r--r--doc/development/rake_tasks.md10
-rw-r--r--doc/development/testing.md34
-rw-r--r--doc/development/ui_guide.md6
-rw-r--r--doc/development/ux_guide/components.md14
-rw-r--r--doc/development/ux_guide/copy.md381
-rw-r--r--doc/development/ux_guide/img/copy-form-addissuebutton.pngbin16085 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissueform.pngbin25978 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissuebutton.pngbin11801 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissueform.pngbin25621 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/harry-robison.pngbin0 -> 10712 bytes
-rw-r--r--doc/development/ux_guide/img/james-mackey.pngbin0 -> 11147 bytes
-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/img/steven-lyons.pngbin0 -> 9323 bytes
-rw-r--r--doc/development/ux_guide/users.md170
-rw-r--r--doc/downgrade_ee_to_ce/README.md7
-rw-r--r--doc/gitlab-basics/command-line-commands.md2
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin4196 -> 6978 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin3045 -> 5842 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin8133 -> 24639 bytes
-rw-r--r--doc/install/README.md34
-rw-r--r--doc/install/database_mysql.md79
-rw-r--r--doc/install/digitaloceandocker.md136
-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.md51
-rw-r--r--doc/install/requirements.md15
-rw-r--r--doc/integration/README.md12
-rw-r--r--doc/integration/auth0.md9
-rw-r--r--doc/integration/azure.md6
-rw-r--r--doc/integration/cas.md9
-rw-r--r--doc/integration/crowd.md59
-rw-r--r--doc/integration/external-issue-tracker.md8
-rw-r--r--doc/integration/facebook.md6
-rw-r--r--doc/integration/github.md24
-rw-r--r--doc/integration/gitlab.md16
-rw-r--r--doc/integration/google.md6
-rw-r--r--doc/integration/jira.md4
-rw-r--r--doc/integration/ldap.md4
-rw-r--r--doc/integration/oauth2_generic.md65
-rw-r--r--doc/integration/omniauth.md3
-rw-r--r--doc/integration/openid_connect_provider.md47
-rw-r--r--doc/integration/saml.md16
-rw-r--r--doc/integration/shibboleth.md12
-rw-r--r--doc/integration/twitter.md6
-rw-r--r--doc/pages/README.md1
-rw-r--r--doc/pages/administration.md1
-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/profile/preferences.md7
-rw-r--r--doc/project_services/bamboo.md61
-rw-r--r--doc/project_services/bugzilla.md18
-rw-r--r--doc/project_services/builds_emails.md17
-rw-r--r--doc/project_services/emails_on_push.md18
-rw-r--r--doc/project_services/hipchat.md55
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin19203 -> 0 bytes
-rw-r--r--doc/project_services/img/mattermost_config_help.pngbin63138 -> 0 bytes
-rw-r--r--doc/project_services/img/mattermost_configuration.pngbin73502 -> 0 bytes
-rw-r--r--doc/project_services/img/services_templates_redmine_example.pngbin8776 -> 0 bytes
-rw-r--r--doc/project_services/img/slack_configuration.pngbin29825 -> 0 bytes
-rw-r--r--doc/project_services/img/slack_setup.pngbin126412 -> 0 bytes
-rw-r--r--doc/project_services/irker.md52
-rw-r--r--doc/project_services/jira.md209
-rw-r--r--doc/project_services/kubernetes.md64
-rw-r--r--doc/project_services/mattermost.md46
-rw-r--r--doc/project_services/mattermost_slash_commands.md164
-rw-r--r--doc/project_services/project_services.md60
-rw-r--r--doc/project_services/redmine.md22
-rw-r--r--doc/project_services/services_templates.md26
-rw-r--r--doc/project_services/slack.md51
-rw-r--r--doc/project_services/slack_slash_commands.md24
-rw-r--r--doc/raketasks/backup_restore.md67
-rw-r--r--doc/raketasks/features.md2
-rw-r--r--doc/security/webhooks.md4
-rw-r--r--doc/ssh/README.md93
-rw-r--r--doc/university/README.md12
-rw-r--r--doc/university/glossary/README.md74
-rw-r--r--doc/university/support/README.md3
-rwxr-xr-xdoc/university/training/topics/additional_resources.md2
-rwxr-xr-xdoc/university/training/user_training.md2
-rw-r--r--doc/update/2.6-to-3.0.md2
-rw-r--r--doc/update/2.9-to-3.0.md2
-rw-r--r--doc/update/3.0-to-3.1.md2
-rw-r--r--doc/update/3.1-to-4.0.md2
-rw-r--r--doc/update/4.0-to-4.1.md2
-rw-r--r--doc/update/4.1-to-4.2.md2
-rw-r--r--doc/update/4.2-to-5.0.md2
-rw-r--r--doc/update/5.0-to-5.1.md2
-rw-r--r--doc/update/5.1-to-5.2.md2
-rw-r--r--doc/update/5.1-to-5.4.md2
-rw-r--r--doc/update/5.1-to-6.0.md2
-rw-r--r--doc/update/5.2-to-5.3.md2
-rw-r--r--doc/update/5.3-to-5.4.md2
-rw-r--r--doc/update/5.4-to-6.0.md2
-rw-r--r--doc/update/6.0-to-6.1.md2
-rw-r--r--doc/update/6.1-to-6.2.md2
-rw-r--r--doc/update/6.2-to-6.3.md2
-rw-r--r--doc/update/6.3-to-6.4.md2
-rw-r--r--doc/update/6.4-to-6.5.md2
-rw-r--r--doc/update/6.5-to-6.6.md2
-rw-r--r--doc/update/6.6-to-6.7.md2
-rw-r--r--doc/update/6.7-to-6.8.md2
-rw-r--r--doc/update/6.8-to-6.9.md2
-rw-r--r--doc/update/6.9-to-7.0.md2
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md4
-rw-r--r--doc/update/7.1-to-7.2.md4
-rw-r--r--doc/update/7.10-to-7.11.md4
-rw-r--r--doc/update/7.11-to-7.12.md4
-rw-r--r--doc/update/7.12-to-7.13.md4
-rw-r--r--doc/update/7.13-to-7.14.md4
-rw-r--r--doc/update/7.14-to-8.0.md4
-rw-r--r--doc/update/7.2-to-7.3.md4
-rw-r--r--doc/update/7.3-to-7.4.md9
-rw-r--r--doc/update/7.4-to-7.5.md8
-rw-r--r--doc/update/7.5-to-7.6.md13
-rw-r--r--doc/update/7.6-to-7.7.md15
-rw-r--r--doc/update/7.7-to-7.8.md13
-rw-r--r--doc/update/7.8-to-7.9.md13
-rw-r--r--doc/update/7.9-to-7.10.md13
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/update/8.12-to-8.13.md4
-rw-r--r--doc/update/8.13-to-8.14.md4
-rw-r--r--doc/update/8.14-to-8.15.md4
-rw-r--r--doc/update/8.15-to-8.16.md4
-rw-r--r--doc/update/8.16-to-8.17.md256
-rw-r--r--doc/update/8.17-to-9.0.md321
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md6
-rw-r--r--doc/update/8.4-to-8.5.md6
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/user/account/security.md2
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md28
-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/admin_area/settings/sign_up_restrictions.md24
-rw-r--r--doc/user/markdown.md70
-rw-r--r--doc/user/permissions.md25
-rw-r--r--doc/user/profile/account/two_factor_authentication.md4
-rw-r--r--doc/user/project/builds/artifacts.md137
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser.pngbin3782 -> 0 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser_button.pngbin4891 -> 0 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_builds_page.pngbin22022 -> 0 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_pipelines_page.pngbin28339 -> 0 bytes
-rw-r--r--doc/user/project/container_registry.md11
-rw-r--r--doc/user/project/img/issue_board.pngbin90664 -> 76461 bytes
-rw-r--r--doc/user/project/img/issue_board_search_backlog.pngbin9769 -> 0 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin97419 -> 120751 bytes
-rw-r--r--doc/user/project/img/issue_boards_add_issues_modal.pngbin0 -> 177057 bytes
-rw-r--r--doc/user/project/img/issue_boards_remove_issue.pngbin0 -> 135168 bytes
-rw-r--r--doc/user/project/img/protected_branches_devs_can_push.pngbin8302 -> 34888 bytes
-rw-r--r--doc/user/project/integrations/bamboo.md59
-rw-r--r--doc/user/project/integrations/bugzilla.md18
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/emails_on_push.md20
-rw-r--r--doc/user/project/integrations/hipchat.md53
-rw-r--r--doc/user/project/integrations/img/accessing_integrations.pngbin0 -> 8941 bytes
-rw-r--r--doc/user/project/integrations/img/emails_on_push_service.png (renamed from doc/project_services/img/emails_on_push_service.png)bin28535 -> 28535 bytes
-rw-r--r--doc/user/project/integrations/img/jira_add_user_to_group.png (renamed from doc/project_services/img/jira_add_user_to_group.png)bin24838 -> 24838 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group.png (renamed from doc/project_services/img/jira_create_new_group.png)bin19127 -> 19127 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group_name.png (renamed from doc/project_services/img/jira_create_new_group_name.png)bin5168 -> 5168 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_user.png (renamed from doc/project_services/img/jira_create_new_user.png)bin12625 -> 12625 bytes
-rw-r--r--doc/user/project/integrations/img/jira_group_access.png (renamed from doc/project_services/img/jira_group_access.png)bin19235 -> 19235 bytes
-rw-r--r--doc/user/project/integrations/img/jira_issue_reference.png (renamed from doc/project_services/img/jira_issue_reference.png)bin18399 -> 18399 bytes
-rw-r--r--doc/user/project/integrations/img/jira_merge_request_close.png (renamed from doc/project_services/img/jira_merge_request_close.png)bin21172 -> 21172 bytes
-rw-r--r--doc/user/project/integrations/img/jira_project_name.png (renamed from doc/project_services/img/jira_project_name.png)bin26685 -> 26685 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service.png (renamed from doc/project_services/img/jira_service.png)bin37869 -> 37869 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_comment.png (renamed from doc/project_services/img/jira_service_close_comment.png)bin11893 -> 11893 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_issue.png (renamed from doc/project_services/img/jira_service_close_issue.png)bin30570 -> 30570 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_page.png (renamed from doc/project_services/img/jira_service_page.png)bin12228 -> 12228 bytes
-rw-r--r--doc/user/project/integrations/img/jira_user_management_link.png (renamed from doc/project_services/img/jira_user_management_link.png)bin23921 -> 23921 bytes
-rw-r--r--doc/user/project/integrations/img/jira_workflow_screenshot.png (renamed from doc/project_services/img/jira_workflow_screenshot.png)bin66685 -> 66685 bytes
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.png (renamed from doc/project_services/img/kubernetes_configuration.png)bin113827 -> 113827 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_add_slash_command.png (renamed from doc/project_services/img/mattermost_add_slash_command.png)bin9265 -> 9265 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_auth.png (renamed from doc/project_services/img/mattermost_bot_auth.png)bin8676 -> 8676 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_available_commands.png (renamed from doc/project_services/img/mattermost_bot_available_commands.png)bin4647 -> 4647 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_config_help.pngbin0 -> 102890 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.pngbin0 -> 249592 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_console_integrations.png (renamed from doc/project_services/img/mattermost_console_integrations.png)bin314642 -> 314642 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_gitlab_token.png (renamed from doc/project_services/img/mattermost_gitlab_token.png)bin3688 -> 3688 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_goto_console.png (renamed from doc/project_services/img/mattermost_goto_console.png)bin7754 -> 7754 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_configuration.png (renamed from doc/project_services/img/mattermost_slash_command_configuration.png)bin24169 -> 24169 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_token.png (renamed from doc/project_services/img/mattermost_slash_command_token.png)bin8624 -> 8624 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_team_integrations.png (renamed from doc/project_services/img/mattermost_team_integrations.png)bin4766 -> 4766 bytes
-rw-r--r--doc/user/project/integrations/img/project_services.pngbin0 -> 25753 bytes
-rw-r--r--doc/user/project/integrations/img/redmine_configuration.png (renamed from doc/project_services/img/redmine_configuration.png)bin10266 -> 10266 bytes
-rw-r--r--doc/user/project/integrations/img/services_templates_redmine_example.pngbin0 -> 8608 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.pngbin0 -> 229050 bytes
-rw-r--r--doc/user/project/integrations/img/slack_setup.pngbin0 -> 86314 bytes
-rw-r--r--doc/user/project/integrations/img/webhooks_ssl.png (renamed from doc/web_hooks/ssl.png)bin27799 -> 27799 bytes
-rw-r--r--doc/user/project/integrations/index.md26
-rw-r--r--doc/user/project/integrations/irker.md50
-rw-r--r--doc/user/project/integrations/jira.md209
-rw-r--r--doc/user/project/integrations/kubernetes.md67
-rw-r--r--doc/user/project/integrations/mattermost.md47
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md164
-rw-r--r--doc/user/project/integrations/mock_ci.md13
-rw-r--r--doc/user/project/integrations/project_services.md76
-rw-r--r--doc/user/project/integrations/redmine.md23
-rw-r--r--doc/user/project/integrations/services_templates.md26
-rw-r--r--doc/user/project/integrations/slack.md53
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md24
-rw-r--r--doc/user/project/integrations/webhooks.md1028
-rw-r--r--doc/user/project/issue_board.md51
-rw-r--r--doc/user/project/merge_requests.md170
-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/index.md169
-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.md23
-rw-r--r--doc/user/project/merge_requests/versions.md17
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md82
-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.pngbin0 -> 6063 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin0 -> 14435 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin0 -> 5351 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin0 -> 12936 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin0 -> 8763 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin0 -> 3810 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin0 -> 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.md49
-rw-r--r--doc/user/project/pages/introduction.md447
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_browser.pngbin0 -> 3771 bytes
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_browser_button.pngbin0 -> 5534 bytes
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_builds_page.pngbin0 -> 15191 bytes
-rw-r--r--doc/user/project/pipelines/img/job_artifacts_pipelines_page.pngbin0 -> 16550 bytes
-rw-r--r--doc/user/project/pipelines/img/job_latest_artifacts_browser.png (renamed from doc/user/project/builds/img/build_latest_artifacts_browser.png)bin10551 -> 10551 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_settings_test_coverage.pngbin2603 -> 2549 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.pngbin6391 -> 6375 bytes
-rw-r--r--doc/user/project/pipelines/job_artifacts.md143
-rw-r--r--doc/user/project/pipelines/settings.md16
-rw-r--r--doc/user/project/repository/web_editor.md3
-rw-r--r--doc/user/project/settings/import_export.md7
-rw-r--r--doc/user/project/slash_commands.md6
-rw-r--r--doc/user/snippets.md19
-rw-r--r--doc/web_hooks/web_hooks.md1026
-rw-r--r--doc/workflow/README.md3
-rw-r--r--doc/workflow/gitlab_flow.md4
-rw-r--r--doc/workflow/groups.md2
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/importing/import_projects_from_github.md6
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md13
-rw-r--r--doc/workflow/shortcuts.md6
-rw-r--r--doc/workflow/todos.md33
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/dashboard/issues.feature21
-rw-r--r--features/project/active_tab.feature52
-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/labels.feature15
-rw-r--r--features/project/merge_requests.feature7
-rw-r--r--features/project/merge_requests/revert.feature1
-rw-r--r--features/project/pages.feature82
-rw-r--r--features/project/shortcuts.feature15
-rw-r--r--features/snippets/user.feature34
-rw-r--r--features/steps/dashboard/issues.rb91
-rw-r--r--features/steps/dashboard/todos.rb23
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/group/milestones.rb4
-rw-r--r--features/steps/project/active_tab.rb50
-rw-r--r--features/steps/project/builds/artifacts.rb2
-rw-r--r--features/steps/project/builds/summary.rb2
-rw-r--r--features/steps/project/commits/branches.rb24
-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.rb10
-rw-r--r--features/steps/project/issues/award_emoji.rb6
-rw-r--r--features/steps/project/labels.rb32
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--features/steps/project/network_graph.rb2
-rw-r--r--features/steps/project/pages.rb139
-rw-r--r--features/steps/project/source/browse_files.rb6
-rw-r--r--features/steps/shared/builds.rb6
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--features/steps/shared/paths.rb2
-rw-r--r--features/steps/shared/project.rb14
-rw-r--r--features/steps/shared/project_tab.rb32
-rw-r--r--features/steps/snippets/user.rb55
-rw-r--r--features/support/capybara.rb2
-rw-r--r--fixtures/emojis/digests.json15206
-rw-r--r--fixtures/emojis/emoji-unicode-version-map.json2377
-rw-r--r--lib/additional_email_headers_interceptor.rb8
-rw-r--r--lib/api/api.rb49
-rw-r--r--lib/api/api_guard.rb17
-rw-r--r--lib/api/award_emoji.rb28
-rw-r--r--lib/api/boards.rb19
-rw-r--r--lib/api/branches.rb24
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/builds.rb261
-rw-r--r--lib/api/commit_statuses.rb11
-rw-r--r--lib/api/commits.rb55
-rw-r--r--lib/api/deploy_keys.rb172
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb229
-rw-r--r--lib/api/environments.rb19
-rw-r--r--lib/api/files.rb78
-rw-r--r--lib/api/groups.rb28
-rw-r--r--lib/api/helpers.rb89
-rw-r--r--lib/api/helpers/internal_helpers.rb10
-rw-r--r--lib/api/helpers/pagination.rb2
-rw-r--r--lib/api/helpers/runner.rb77
-rw-r--r--lib/api/internal.rb12
-rw-r--r--lib/api/issues.rb70
-rw-r--r--lib/api/jobs.rb252
-rw-r--r--lib/api/labels.rb10
-rw-r--r--lib/api/members.rb45
-rw-r--r--lib/api/merge_request_diffs.rb18
-rw-r--r--lib/api/merge_requests.rb295
-rw-r--r--lib/api/milestones.rb39
-rw-r--r--lib/api/notes.rb8
-rw-r--r--lib/api/pagination_params.rb4
-rw-r--r--lib/api/pipelines.rb10
-rw-r--r--lib/api/project_hooks.rb13
-rw-r--r--lib/api/project_snippets.rb24
-rw-r--r--lib/api/projects.rb133
-rw-r--r--lib/api/repositories.rb71
-rw-r--r--lib/api/runner.rb250
-rw-r--r--lib/api/runners.rb10
-rw-r--r--lib/api/services.rb38
-rw-r--r--lib/api/settings.rb21
-rw-r--r--lib/api/snippets.rb26
-rw-r--r--lib/api/subscriptions.rb5
-rw-r--r--lib/api/system_hooks.rb12
-rw-r--r--lib/api/tags.rb20
-rw-r--r--lib/api/templates.rb103
-rw-r--r--lib/api/time_tracking_endpoints.rb6
-rw-r--r--lib/api/todos.rb16
-rw-r--r--lib/api/triggers.rb89
-rw-r--r--lib/api/users.rb121
-rw-r--r--lib/api/v3/award_emoji.rb130
-rw-r--r--lib/api/v3/boards.rb72
-rw-r--r--lib/api/v3/branches.rb51
-rw-r--r--lib/api/v3/broadcast_messages.rb31
-rw-r--r--lib/api/v3/builds.rb255
-rw-r--r--lib/api/v3/commits.rb196
-rw-r--r--lib/api/v3/deploy_keys.rb122
-rw-r--r--lib/api/v3/deployments.rb43
-rw-r--r--lib/api/v3/entities.rb253
-rw-r--r--lib/api/v3/environments.rb87
-rw-r--r--lib/api/v3/files.rb138
-rw-r--r--lib/api/v3/groups.rb181
-rw-r--r--lib/api/v3/helpers.rb19
-rw-r--r--lib/api/v3/issues.rb231
-rw-r--r--lib/api/v3/labels.rb34
-rw-r--r--lib/api/v3/members.rb134
-rw-r--r--lib/api/v3/merge_request_diffs.rb43
-rw-r--r--lib/api/v3/merge_requests.rb290
-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.rb143
-rw-r--r--lib/api/v3/projects.rb474
-rw-r--r--lib/api/v3/repositories.rb109
-rw-r--r--lib/api/v3/runners.rb65
-rw-r--r--lib/api/v3/services.rb641
-rw-r--r--lib/api/v3/settings.rb137
-rw-r--r--lib/api/v3/snippets.rb138
-rw-r--r--lib/api/v3/subscriptions.rb53
-rw-r--r--lib/api/v3/system_hooks.rb32
-rw-r--r--lib/api/v3/tags.rb40
-rw-r--r--lib/api/v3/templates.rb122
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb116
-rw-r--r--lib/api/v3/todos.rb30
-rw-r--r--lib/api/v3/triggers.rb103
-rw-r--r--lib/api/v3/users.rb149
-rw-r--r--lib/api/v3/variables.rb29
-rw-r--r--lib/api/variables.rb6
-rw-r--r--lib/backup/database.rb70
-rw-r--r--lib/backup/files.rb21
-rw-r--r--lib/backup/manager.rb27
-rw-r--r--lib/backup/pages.rb13
-rw-r--r--lib/backup/repository.rb27
-rw-r--r--lib/backup/uploads.rb1
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb20
-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.rb9
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb11
-rw-r--r--lib/banzai/filter/plantuml_filter.rb39
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb8
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/querying.rb56
-rw-r--r--lib/banzai/reference_extractor.rb5
-rw-r--r--lib/banzai/reference_parser/base_parser.rb7
-rw-r--r--lib/banzai/reference_parser/directly_addressed_user_parser.rb8
-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.rb52
-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/api/triggers.rb43
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb4
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-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/asciidoc.rb3
-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/badge/metadata.rb4
-rw-r--r--lib/gitlab/changes_list.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/issuable.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_base.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb6
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_search.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_show.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/configurable.rb2
-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/global.rb5
-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.rb22
-rw-r--r--lib/gitlab/conflict/resolution_error.rb3
-rw-r--r--lib/gitlab/contributions_calendar.rb6
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb4
-rw-r--r--lib/gitlab/data_builder/build.rb10
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database.rb50
-rw-r--r--lib/gitlab/database/median.rb1
-rw-r--r--lib/gitlab/database/migration_helpers.rb59
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb2
-rw-r--r--lib/gitlab/diff/parser.rb4
-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/handler/create_issue_handler.rb2
-rw-r--r--lib/gitlab/email/message/repository_push.rb4
-rw-r--r--lib/gitlab/email/receiver.rb47
-rw-r--r--lib/gitlab/email/reply_parser.rb11
-rw-r--r--lib/gitlab/emoji.rb43
-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.rb2
-rw-r--r--lib/gitlab/git/blob.rb157
-rw-r--r--lib/gitlab/git/blob_snippet.rb2
-rw-r--r--lib/gitlab/git/commit.rb6
-rw-r--r--lib/gitlab/git/diff.rb2
-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_post_receive.rb4
-rw-r--r--lib/gitlab/gitaly_client.rb29
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb17
-rw-r--r--lib/gitlab/github_import/base_formatter.rb18
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb2
-rw-r--r--lib/gitlab/github_import/client.rb8
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb10
-rw-r--r--lib/gitlab/github_import/importer.rb23
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb30
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb14
-rw-r--r--lib/gitlab/github_import/user_formatter.rb45
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/google_code_import/importer.rb2
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/command_line_util.rb12
-rw-r--r--lib/gitlab/import_export/error.rb2
-rw-r--r--lib/gitlab/import_export/importer.rb2
-rw-r--r--lib/gitlab/import_export/members_mapper.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb33
-rw-r--r--lib/gitlab/import_export/reader.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb21
-rw-r--r--lib/gitlab/incoming_email.rb12
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/ldap/person.rb4
-rw-r--r--lib/gitlab/metrics.rb6
-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/system.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.rb24
-rw-r--r--lib/gitlab/o_auth/user.rb13
-rw-r--r--lib/gitlab/optimistic_locking.rb6
-rw-r--r--lib/gitlab/other_markup.rb3
-rw-r--r--lib/gitlab/pages_transfer.rb7
-rw-r--r--lib/gitlab/project_transfer.rb35
-rw-r--r--lib/gitlab/prometheus.rb70
-rw-r--r--lib/gitlab/recaptcha.rb4
-rw-r--r--lib/gitlab/redis.rb21
-rw-r--r--lib/gitlab/reference_extractor.rb8
-rw-r--r--lib/gitlab/regex.rb13
-rw-r--r--lib/gitlab/request_context.rb21
-rw-r--r--lib/gitlab/request_profiler.rb2
-rw-r--r--lib/gitlab/request_profiler/middleware.rb3
-rw-r--r--lib/gitlab/route_map.rb50
-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/serialize/ci/variables.rb27
-rw-r--r--lib/gitlab/serializer/ci/variables.rb27
-rw-r--r--lib/gitlab/serializer/pagination.rb36
-rw-r--r--lib/gitlab/shell.rb12
-rw-r--r--lib/gitlab/sherlock/query.rb11
-rw-r--r--lib/gitlab/sidekiq_status.rb35
-rw-r--r--lib/gitlab/sidekiq_status/client_middleware.rb2
-rw-r--r--lib/gitlab/sidekiq_status/server_middleware.rb2
-rw-r--r--lib/gitlab/slash_commands/extractor.rb2
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-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/themes.rb87
-rw-r--r--lib/gitlab/update_path_error.rb2
-rw-r--r--lib/gitlab/upgrader.rb15
-rw-r--r--lib/gitlab/uploads_transfer.rb30
-rw-r--r--lib/gitlab/url_sanitizer.rb4
-rw-r--r--lib/gitlab/user_access.rb14
-rw-r--r--lib/gitlab/visibility_level.rb55
-rw-r--r--lib/gitlab/workhorse.rb24
-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/rouge/lexers/plantuml.rb21
-rwxr-xr-xlib/support/init.d/gitlab68
-rw-r--r--[-rwxr-xr-x]lib/support/init.d/gitlab.default.example24
-rw-r--r--lib/support/nginx/gitlab-pages28
-rw-r--r--lib/support/nginx/gitlab-pages-ssl77
-rw-r--r--lib/support/nginx/gitlab-ssl3
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/config_lint.rake25
-rw-r--r--lib/tasks/dev.rake2
-rw-r--r--lib/tasks/downtime_check.rake10
-rw-r--r--lib/tasks/eslint.rake7
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gemojione.rake93
-rw-r--r--lib/tasks/gitlab/assets.rake23
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--lib/tasks/gitlab/check.rake81
-rw-r--r--lib/tasks/gitlab/cleanup.rake19
-rw-r--r--lib/tasks/gitlab/db.rake8
-rw-r--r--lib/tasks/gitlab/git.rake2
-rw-r--r--lib/tasks/gitlab/import.rake7
-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.rake8
-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/grape.rake6
-rw-r--r--lib/tasks/karma.rake20
-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--lib/tasks/teaspoon.rake25
-rw-r--r--lib/tasks/test.rake2
-rw-r--r--lib/tasks/yarn.rake40
-rw-r--r--package.json59
-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/.rspec3
-rw-r--r--qa/Dockerfile14
-rw-r--r--qa/Gemfile7
-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.rb26
-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.rb27
-rw-r--r--rubocop/cop/migration/add_column.rb52
-rw-r--r--rubocop/cop/migration/add_column_with_default.rb34
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb27
-rw-r--r--rubocop/cop/migration/add_concurrent_index.rb34
-rw-r--r--rubocop/cop/migration/add_index.rb4
-rw-r--r--rubocop/cop/migration/column_with_default.rb50
-rw-r--r--rubocop/rubocop.rb9
-rw-r--r--shared/pages/.gitkeep0
-rw-r--r--spec/config/mail_room_spec.rb4
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb65
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb85
-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.rb25
-rw-r--r--spec/controllers/dashboard_controller_spec.rb19
-rw-r--r--spec/controllers/health_check_controller_spec.rb4
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb29
-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/profiles/preferences_controller_spec.rb6
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb4
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb73
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb81
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb12
-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.rb111
-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.rb296
-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.rb165
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb27
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb64
-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/runners_controller_spec.rb75
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb20
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb20
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb249
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb4
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb11
-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.rb85
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb59
-rw-r--r--spec/controllers/projects_controller_spec.rb87
-rw-r--r--spec/controllers/registrations_controller_spec.rb2
-rw-r--r--spec/controllers/root_controller_spec.rb36
-rw-r--r--spec/controllers/search_controller_spec.rb16
-rw-r--r--spec/controllers/sessions_controller_spec.rb12
-rw-r--r--spec/controllers/snippets_controller_spec.rb177
-rw-r--r--spec/controllers/uploads_controller_spec.rb104
-rw-r--r--spec/factories/boards.rb1
-rw-r--r--spec/factories/chat_teams.rb9
-rw-r--r--spec/factories/ci/builds.rb74
-rw-r--r--spec/factories/ci/pipelines.rb8
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/commits.rb10
-rw-r--r--spec/factories/events.rb12
-rw-r--r--spec/factories/keys.rb7
-rw-r--r--spec/factories/lists.rb6
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/factories/notes.rb13
-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/pages_domains.rb153
-rw-r--r--spec/factories/personal_access_tokens.rb17
-rw-r--r--spec/factories/projects.rb82
-rw-r--r--spec/factories/services.rb19
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/factories/todos.rb22
-rw-r--r--spec/factories/users.rb13
-rw-r--r--spec/factories/wiki_directories.rb6
-rw-r--r--spec/factories/wiki_pages.rb18
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb19
-rw-r--r--spec/features/admin/admin_builds_spec.rb34
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb2
-rw-r--r--spec/features/admin/admin_labels_spec.rb11
-rw-r--r--spec/features/admin/admin_runners_spec.rb2
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb72
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb2
-rw-r--r--spec/features/atom/issues_spec.rb4
-rw-r--r--spec/features/atom/users_spec.rb4
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb239
-rw-r--r--spec/features/boards/boards_spec.rb245
-rw-r--r--spec/features/boards/issue_ordering_spec.rb166
-rw-r--r--spec/features/boards/modal_filter_spec.rb259
-rw-r--r--spec/features/boards/new_issue_spec.rb5
-rw-r--r--spec/features/boards/sidebar_spec.rb192
-rw-r--r--spec/features/calendar_spec.rb222
-rw-r--r--spec/features/commits_spec.rb6
-rw-r--r--spec/features/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/dashboard/active_tab_spec.rb7
-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/issuables_counter_spec.rb5
-rw-r--r--spec/features/dashboard/issues_spec.rb51
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb10
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb10
-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.rb197
-rw-r--r--spec/features/environments_spec.rb229
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb28
-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/issues_spec.rb18
-rw-r--r--spec/features/groups/members/list_spec.rb55
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/show_spec.rb24
-rw-r--r--spec/features/groups_spec.rb59
-rw-r--r--spec/features/help_pages_spec.rb26
-rw-r--r--spec/features/issuables/issuable_list_spec.rb75
-rw-r--r--spec/features/issues/award_emoji_spec.rb35
-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.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb63
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb239
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb86
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb436
-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/gfm_autocomplete_spec.rb14
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb65
-rw-r--r--spec/features/issues/spam_issues_spec.rb66
-rw-r--r--spec/features/issues_spec.rb57
-rw-r--r--spec/features/login_spec.rb28
-rw-r--r--spec/features/markdown_spec.rb8
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb32
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb4
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb23
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb186
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb86
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb38
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb321
-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/mini_pipeline_graph_spec.rb100
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb12
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb61
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb76
-rw-r--r--spec/features/merge_requests/widget_spec.rb156
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/milestones/milestones_spec.rb5
-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.rb22
-rw-r--r--spec/features/profiles/preferences_spec.rb29
-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/badges/list_spec.rb6
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb97
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb37
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb107
-rw-r--r--spec/features/projects/builds_spec.rb50
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb90
-rw-r--r--spec/features/projects/commit/rss_spec.rb27
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb89
-rw-r--r--spec/features/projects/compare_spec.rb (renamed from spec/features/compare_spec.rb)0
-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/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb11
-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/issuable_templates_spec.rb35
-rw-r--r--spec/features/projects/issues/rss_spec.rb31
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb8
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb3
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb7
-rw-r--r--spec/features/projects/main/rss_spec.rb25
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb16
-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.rb45
-rw-r--r--spec/features/projects/pages_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb54
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb136
-rw-r--r--spec/features/projects/ref_switcher_spec.rb13
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb56
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb21
-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/view_on_env_spec.rb140
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb12
-rw-r--r--spec/features/projects_spec.rb2
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb12
-rw-r--r--spec/features/search_spec.rb13
-rw-r--r--spec/features/security/project/internal_access_spec.rb28
-rw-r--r--spec/features/security/project/private_access_spec.rb28
-rw-r--r--spec/features/security/project/public_access_spec.rb28
-rw-r--r--spec/features/snippets/user_snippets_spec.rb49
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb27
-rw-r--r--spec/features/todos/todos_filtering_spec.rb57
-rw-r--r--spec/features/todos/todos_sorting_spec.rb8
-rw-r--r--spec/features/todos/todos_spec.rb127
-rw-r--r--spec/features/triggers_spec.rb171
-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/features/variables_spec.rb30
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb13
-rw-r--r--spec/finders/environments_finder_spec.rb110
-rw-r--r--spec/finders/group_members_finder_spec.rb32
-rw-r--r--spec/finders/issues_finder_spec.rb8
-rw-r--r--spec/finders/members_finder_spec.rb22
-rw-r--r--spec/finders/merge_requests_finder_spec.rb8
-rw-r--r--spec/finders/notes_finder_spec.rb12
-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.json2
-rw-r--r--spec/fixtures/api/schemas/list.json2
-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.json36
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json77
-rw-r--r--spec/fixtures/api/schemas/user/login.json37
-rw-r--r--spec/fixtures/api/schemas/user/public.json79
-rw-r--r--spec/fixtures/config/mail_room_disabled.yml (renamed from spec/fixtures/mail_room_disabled.yml)0
-rw-r--r--spec/fixtures/config/mail_room_enabled.yml (renamed from spec/fixtures/mail_room_enabled.yml)0
-rw-r--r--spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml42
-rw-r--r--spec/fixtures/markdown.md.erb5
-rw-r--r--spec/fixtures/pages.tar.gzbin0 -> 1795 bytes
-rw-r--r--spec/fixtures/pages.zipbin0 -> 1851 bytes
-rw-r--r--spec/fixtures/pages.zip.metabin0 -> 225 bytes
-rw-r--r--spec/fixtures/pages_empty.tar.gzbin0 -> 128 bytes
-rw-r--r--spec/fixtures/pages_empty.zipbin0 -> 160 bytes
-rw-r--r--spec/fixtures/pages_empty.zip.metabin0 -> 116 bytes
-rw-r--r--spec/helpers/application_helper_spec.rb15
-rw-r--r--spec/helpers/auth_helper_spec.rb2
-rw-r--r--spec/helpers/commits_helper_spec.rb19
-rw-r--r--spec/helpers/emails_helper_spec.rb32
-rw-r--r--spec/helpers/events_helper_spec.rb7
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb2
-rw-r--r--spec/helpers/issues_helper_spec.rb38
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb92
-rw-r--r--spec/helpers/milestones_helper_spec.rb2
-rw-r--r--spec/helpers/page_layout_helper_spec.rb12
-rw-r--r--spec/helpers/preferences_helper_spec.rb26
-rw-r--r--spec/helpers/projects_helper_spec.rb1
-rw-r--r--spec/helpers/rss_helper_spec.rb20
-rw-r--r--spec/helpers/submodule_helper_spec.rb44
-rw-r--r--spec/helpers/version_check_helper_spec.rb34
-rw-r--r--spec/helpers/wiki_helper_spec.rb21
-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/.eslintrc8
-rw-r--r--spec/javascripts/abuse_reports_spec.js43
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es643
-rw-r--r--spec/javascripts/activities_spec.js62
-rw-r--r--spec/javascripts/activities_spec.js.es663
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js58
-rw-r--r--spec/javascripts/awards_handler_spec.js192
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js6
-rw-r--r--spec/javascripts/behaviors/bind_in_out_spec.js189
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js52
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js23
-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_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.es6178
-rw-r--r--spec/javascripts/boards/issue_card_spec.js191
-rw-r--r--spec/javascripts/boards/issue_spec.js98
-rw-r--r--spec/javascripts/boards/issue_spec.js.es687
-rw-r--r--spec/javascripts/boards/list_spec.js109
-rw-r--r--spec/javascripts/boards/list_spec.js.es692
-rw-r--r--spec/javascripts/boards/mock_data.js63
-rw-r--r--spec/javascripts/boards/mock_data.js.es658
-rw-r--r--spec/javascripts/boards/modal_store_spec.js132
-rw-r--r--spec/javascripts/bootstrap_jquery_spec.js42
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js71
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js.es659
-rw-r--r--spec/javascripts/build_spec.js177
-rw-r--r--spec/javascripts/build_spec.js.es6184
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js92
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js105
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js33
-rw-r--r--spec/javascripts/commits_spec.js62
-rw-r--r--spec/javascripts/commits_spec.js.es650
-rw-r--r--spec/javascripts/dashboard_spec.js.es639
-rw-r--r--spec/javascripts/datetime_utility_spec.js65
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es665
-rw-r--r--spec/javascripts/diff_comments_store_spec.js133
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es6125
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js47
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es667
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js22
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es622
-rw-r--r--spec/javascripts/environments/environment_item_spec.js212
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es6229
-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.es6127
-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_terminal_button_spec.js24
-rw-r--r--spec/javascripts/environments/environments_store_spec.js58
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es671
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js202
-rw-r--r--spec/javascripts/environments/mock_data.js86
-rw-r--r--spec/javascripts/environments/mock_data.js.es6149
-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.es640
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js283
-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.js250
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es669
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js110
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6110
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js127
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6104
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js600
-rw-r--r--spec/javascripts/fixtures/.gitignore1
-rw-r--r--spec/javascripts/fixtures/ajax_loading_spinner.html.haml2
-rw-r--r--spec/javascripts/fixtures/behaviors/quick_submit.html.haml6
-rw-r--r--spec/javascripts/fixtures/behaviors/requires_input.html.haml18
-rw-r--r--spec/javascripts/fixtures/branches.rb28
-rw-r--r--spec/javascripts/fixtures/builds.rb2
-rw-r--r--spec/javascripts/fixtures/dashboard.html.haml45
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js4
-rw-r--r--spec/javascripts/fixtures/environments/environments_folder_view.html.haml7
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml12
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml2
-rw-r--r--spec/javascripts/fixtures/header.html.haml35
-rw-r--r--spec/javascripts/fixtures/issues.rb2
-rw-r--r--spec/javascripts/fixtures/merge_request_tabs.html.haml22
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb36
-rw-r--r--spec/javascripts/fixtures/new_branch.html.haml4
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml2
-rw-r--r--spec/javascripts/fixtures/project_branches.json5
-rw-r--r--spec/javascripts/fixtures/project_title.html.haml20
-rw-r--r--spec/javascripts/fixtures/projects.json18
-rw-r--r--spec/javascripts/fixtures/projects.rb28
-rw-r--r--spec/javascripts/fixtures/target_branch_dropdown.html.haml28
-rw-r--r--spec/javascripts/fixtures/todos.json4
-rw-r--r--spec/javascripts/fixtures/todos.rb52
-rw-r--r--spec/javascripts/fixtures/user_callout.html.haml2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js148
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js.es691
-rw-r--r--spec/javascripts/gl_dropdown_spec.js196
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6189
-rw-r--r--spec/javascripts/gl_emoji_spec.js363
-rw-r--r--spec/javascripts/gl_field_errors_spec.js110
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es6111
-rw-r--r--spec/javascripts/gl_form_spec.js123
-rw-r--r--spec/javascripts/gl_form_spec.js.es6122
-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/header_spec.js14
-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.js36
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js.es635
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js52
-rw-r--r--spec/javascripts/issuable_spec.js80
-rw-r--r--spec/javascripts/issuable_spec.js.es681
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js202
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js.es6201
-rw-r--r--spec/javascripts/issue_spec.js49
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js90
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es692
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js167
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es673
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js110
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es625
-rw-r--r--spec/javascripts/line_highlighter_spec.js22
-rw-r--r--spec/javascripts/merge_request_spec.js10
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js118
-rw-r--r--spec/javascripts/merge_request_widget_spec.js8
-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.js9
-rw-r--r--spec/javascripts/notes_spec.js22
-rw-r--r--spec/javascripts/pager_spec.js90
-rw-r--r--spec/javascripts/pipelines_spec.js30
-rw-r--r--spec/javascripts/pipelines_spec.js.es625
-rw-r--r--spec/javascripts/polyfills/element_spec.js36
-rw-r--r--spec/javascripts/pretty_time_spec.js134
-rw-r--r--spec/javascripts/pretty_time_spec.js.es6134
-rw-r--r--spec/javascripts/project_title_spec.js52
-rw-r--r--spec/javascripts/right_sidebar_spec.js13
-rw-r--r--spec/javascripts/search_autocomplete_spec.js46
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js20
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js53
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js.es653
-rw-r--r--spec/javascripts/smart_interval_spec.js179
-rw-r--r--spec/javascripts/smart_interval_spec.js.es6180
-rw-r--r--spec/javascripts/spec_helper.js48
-rw-r--r--spec/javascripts/subbable_resource_spec.js63
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es666
-rw-r--r--spec/javascripts/syntax_highlight_spec.js4
-rw-r--r--spec/javascripts/test_bundle.js68
-rw-r--r--spec/javascripts/todos_spec.js63
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js28
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js12
-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.js100
-rw-r--r--spec/javascripts/visibility_select_spec.js.es6100
-rw-r--r--spec/javascripts/vue_common_components/commit_spec.js.es6131
-rw-r--r--spec/javascripts/vue_pagination/pagination_spec.js.es6168
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js131
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js87
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js64
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js158
-rw-r--r--spec/javascripts/zen_mode_spec.js4
-rw-r--r--spec/lib/additional_email_headers_interceptor_spec.rb12
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb2
-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/issue_reference_filter_spec.rb24
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb32
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb19
-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.rb111
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb4
-rw-r--r--spec/lib/event_filter_spec.rb18
-rw-r--r--spec/lib/expand_variables_spec.rb27
-rw-r--r--spec/lib/extracts_path_spec.rb4
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb23
-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/badge/shared/metadata.rb10
-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/factory_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb26
-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/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb45
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb26
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb86
-rw-r--r--spec/lib/gitlab/database_spec.rb85
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb12
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb6
-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/index_spec.rb220
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb269
-rw-r--r--spec/lib/gitlab/git_access_spec.rb30
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/git_spec.rb6
-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/comment_formatter_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb123
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb75
-rw-r--r--spec/lib/gitlab/github_import/user_formatter_spec.rb39
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml8
-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.rb5
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project.json18
-rw-r--r--spec/lib/gitlab/import_export/project.light.json48
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb165
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb74
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml9
-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/incoming_email_spec.rb15
-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/other_markup.rb22
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/project_transfer_spec.rb51
-rw-r--r--spec/lib/gitlab/prometheus_spec.rb143
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb75
-rw-r--r--spec/lib/gitlab/regex_spec.rb81
-rw-r--r--spec/lib/gitlab/request_context_spec.rb30
-rw-r--r--spec/lib/gitlab/route_map_spec.rb90
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb21
-rw-r--r--spec/lib/gitlab/serialize/ci/variables_spec.rb18
-rw-r--r--spec/lib/gitlab/serializer/ci/variables_spec.rb19
-rw-r--r--spec/lib/gitlab/serializer/pagination_spec.rb49
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb26
-rw-r--r--spec/lib/gitlab/slash_commands/extractor_spec.rb8
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb19
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb19
-rw-r--r--spec/lib/gitlab/themes_spec.rb48
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb3
-rw-r--r--spec/lib/gitlab/uploads_transfer_spec.rb50
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb8
-rw-r--r--spec/lib/gitlab/utils_spec.rb4
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb60
-rw-r--r--spec/lib/mattermost/command_spec.rb6
-rw-r--r--spec/lib/mattermost/team_spec.rb29
-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.rb8
-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.rb195
-rw-r--r--spec/models/ci/pipeline_spec.rb105
-rw-r--r--spec/models/ci/runner_spec.rb29
-rw-r--r--spec/models/ci/stage_spec.rb17
-rw-r--r--spec/models/ci/trigger_spec.rb72
-rw-r--r--spec/models/commit_status_spec.rb47
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb5
-rw-r--r--spec/models/concerns/has_status_spec.rb36
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb104
-rw-r--r--spec/models/concerns/routable_spec.rb34
-rw-r--r--spec/models/concerns/spammable_spec.rb19
-rw-r--r--spec/models/concerns/uniquify_spec.rb33
-rw-r--r--spec/models/cycle_analytics/production_spec.rb7
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb11
-rw-r--r--spec/models/deployment_spec.rb4
-rw-r--r--spec/models/environment_spec.rb169
-rw-r--r--spec/models/event_spec.rb24
-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.rb15
-rw-r--r--spec/models/guest_spec.rb2
-rw-r--r--spec/models/list_spec.rb39
-rw-r--r--spec/models/member_spec.rb8
-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.rb111
-rw-r--r--spec/models/namespace_spec.rb74
-rw-r--r--spec/models/note_spec.rb12
-rw-r--r--spec/models/pages_domain_spec.rb168
-rw-r--r--spec/models/personal_access_token_spec.rb60
-rw-r--r--spec/models/project_feature_spec.rb2
-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/chat_message/build_message_spec.rb28
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb10
-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/jira_service_spec.rb2
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb30
-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.rb298
-rw-r--r--spec/models/repository_spec.rb311
-rw-r--r--spec/models/route_spec.rb45
-rw-r--r--spec/models/timelog_spec.rb28
-rw-r--r--spec/models/upload_spec.rb151
-rw-r--r--spec/models/user_spec.rb144
-rw-r--r--spec/models/wiki_directory_spec.rb44
-rw-r--r--spec/models/wiki_page_spec.rb118
-rw-r--r--spec/policies/base_policy_spec.rb10
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb103
-rw-r--r--spec/policies/project_policy_spec.rb62
-rw-r--r--spec/policies/project_snippet_policy_spec.rb101
-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.rb5
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb2
-rw-r--r--spec/requests/api/award_emoji_spec.rb67
-rw-r--r--spec/requests/api/boards_spec.rb5
-rw-r--r--spec/requests/api/branches_spec.rb42
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb8
-rw-r--r--spec/requests/api/builds_spec.rb472
-rw-r--r--spec/requests/api/commit_statuses_spec.rb84
-rw-r--r--spec/requests/api/commits_spec.rb153
-rw-r--r--spec/requests/api/deploy_keys_spec.rb25
-rw-r--r--spec/requests/api/deployments_spec.rb5
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb34
-rw-r--r--spec/requests/api/environments_spec.rb46
-rw-r--r--spec/requests/api/files_spec.rb215
-rw-r--r--spec/requests/api/fork_spec.rb134
-rw-r--r--spec/requests/api/groups_spec.rb93
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb28
-rw-r--r--spec/requests/api/issues_spec.rb454
-rw-r--r--spec/requests/api/jobs_spec.rb480
-rw-r--r--spec/requests/api/labels_spec.rb40
-rw-r--r--spec/requests/api/members_spec.rb33
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb34
-rw-r--r--spec/requests/api/merge_requests_spec.rb268
-rw-r--r--spec/requests/api/milestones_spec.rb136
-rw-r--r--spec/requests/api/namespaces_spec.rb18
-rw-r--r--spec/requests/api/notes_spec.rb19
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb22
-rw-r--r--spec/requests/api/pipelines_spec.rb6
-rw-r--r--spec/requests/api/project_hooks_spec.rb17
-rw-r--r--spec/requests/api/project_snippets_spec.rb119
-rw-r--r--spec/requests/api/projects_spec.rb730
-rw-r--r--spec/requests/api/repositories_spec.rb107
-rw-r--r--spec/requests/api/runner_spec.rb1026
-rw-r--r--spec/requests/api/runners_spec.rb37
-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.rb83
-rw-r--r--spec/requests/api/system_hooks_spec.rb3
-rw-r--r--spec/requests/api/tags_spec.rb14
-rw-r--r--spec/requests/api/templates_spec.rb70
-rw-r--r--spec/requests/api/todos_spec.rb43
-rw-r--r--spec/requests/api/triggers_spec.rb151
-rw-r--r--spec/requests/api/users_spec.rb290
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb299
-rw-r--r--spec/requests/api/v3/boards_spec.rb113
-rw-r--r--spec/requests/api/v3/branches_spec.rb83
-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.rb578
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb172
-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.rb285
-rw-r--r--spec/requests/api/v3/groups_spec.rb565
-rw-r--r--spec/requests/api/v3/issues_spec.rb1293
-rw-r--r--spec/requests/api/v3/labels_spec.rb171
-rw-r--r--spec/requests/api/v3/members_spec.rb342
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb50
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb733
-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/project_snippets_spec.rb228
-rw-r--r--spec/requests/api/v3/projects_spec.rb1452
-rw-r--r--spec/requests/api/v3/repositories_spec.rb366
-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.rb57
-rw-r--r--spec/requests/api/v3/tags_spec.rb89
-rw-r--r--spec/requests/api/v3/templates_spec.rb203
-rw-r--r--spec/requests/api/v3/todos_spec.rb73
-rw-r--r--spec/requests/api/v3/triggers_spec.rb218
-rw-r--r--spec/requests/api/v3/users_spec.rb266
-rw-r--r--spec/requests/api/variables_spec.rb3
-rw-r--r--spec/requests/ci/api/builds_spec.rb37
-rw-r--r--spec/requests/ci/api/runners_spec.rb5
-rw-r--r--spec/requests/ci/api/triggers_spec.rb3
-rw-r--r--spec/requests/git_http_spec.rb24
-rw-r--r--spec/requests/lfs_http_spec.rb177
-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.rb66
-rw-r--r--spec/rubocop/cop/custom_error_class_spec.rb111
-rw-r--r--spec/rubocop/cop/gem_fetcher_spec.rb46
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_spec.rb41
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb33
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb41
-rw-r--r--spec/serializers/environment_serializer_spec.rb143
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb6
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-rw-r--r--spec/services/boards/create_service_spec.rb7
-rw-r--r--spec/services/boards/issues/list_service_spec.rb27
-rw-r--r--spec/services/boards/issues/move_service_spec.rb67
-rw-r--r--spec/services/boards/lists/create_service_spec.rb4
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb9
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb9
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb71
-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.rb713
-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.rb142
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb234
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb4
-rw-r--r--spec/services/ci/update_runner_service_spec.rb41
-rw-r--r--spec/services/compare_service_spec.rb6
-rw-r--r--spec/services/create_deployment_service_spec.rb17
-rw-r--r--spec/services/create_tag_service_spec.rb53
-rw-r--r--spec/services/delete_tag_service_spec.rb17
-rw-r--r--spec/services/delete_user_service_spec.rb60
-rw-r--r--spec/services/destroy_group_service_spec.rb98
-rw-r--r--spec/services/files/update_service_spec.rb33
-rw-r--r--spec/services/git_hooks_service_spec.rb2
-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.rb113
-rw-r--r--spec/services/groups/update_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb67
-rw-r--r--spec/services/issues/create_service_spec.rb174
-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/add_todo_when_build_fails_service_spec.rb25
-rw-r--r--spec/services/merge_requests/build_service_spec.rb13
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb29
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb37
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb149
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb15
-rw-r--r--spec/services/notes/create_service_spec.rb50
-rw-r--r--spec/services/notes/delete_service_spec.rb15
-rw-r--r--spec/services/notes/destroy_service_spec.rb15
-rw-r--r--spec/services/notification_service_spec.rb67
-rw-r--r--spec/services/pages_service_spec.rb47
-rw-r--r--spec/services/projects/create_service_spec.rb8
-rw-r--r--spec/services/projects/destroy_service_spec.rb60
-rw-r--r--spec/services/projects/transfer_service_spec.rb28
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb24
-rw-r--r--spec/services/projects/update_pages_service_spec.rb102
-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.rb71
-rw-r--r--spec/services/spam_service_spec.rb62
-rw-r--r--spec/services/system_note_service_spec.rb20
-rw-r--r--spec/services/tags/create_service_spec.rb53
-rw-r--r--spec/services/tags/destroy_service_spec.rb17
-rw-r--r--spec/services/todo_service_spec.rb248
-rw-r--r--spec/services/users/destroy_spec.rb130
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb74
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb36
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb21
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb37
-rw-r--r--spec/spec_helper.rb14
-rw-r--r--spec/support/api/issues_resolving_discussions_shared_examples.rb15
-rw-r--r--spec/support/api/pagination_shared_examples.rb20
-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.rb13
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/carrierwave.rb4
-rw-r--r--spec/support/cycle_analytics_helpers.rb16
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/drag_to_helper.rb13
-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.rb74
-rw-r--r--spec/support/gitlab_stubs/session.json4
-rw-r--r--spec/support/gitlab_stubs/user.json4
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb36
-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.rb16
-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/gitaly_matchers.rb3
-rw-r--r--spec/support/matchers/markdown_matchers.rb7
-rw-r--r--spec/support/matchers/match_file.rb5
-rw-r--r--spec/support/matchers/pagination_matcher.rb5
-rw-r--r--spec/support/matchers/satisfy_matchers.rb19
-rw-r--r--spec/support/merge_request_helpers.rb9
-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.rb2
-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_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/slash_commands_helpers.rb2
-rw-r--r--spec/support/stub_gitlab_calls.rb2
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/support/time_tracking_shared_examples.rb2
-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/config_lint_spec.rb27
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb39
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb37
-rw-r--r--spec/teaspoon_env.rb178
-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/_home_panel.html.haml_spec.rb38
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb82
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb28
-rw-r--r--spec/views/projects/notes/_form.html.haml_spec.rb4
-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/delete_user_worker_spec.rb4
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb7
-rw-r--r--spec/workers/post_receive_spec.rb8
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
-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/date.format.js207
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js1159
-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.atwho.js1202
-rw-r--r--vendor/assets/javascripts/jquery.caret.js436
-rw-r--r--vendor/assets/javascripts/jquery.highlight.js53
-rw-r--r--vendor/assets/javascripts/jquery.turbolinks.js49
-rw-r--r--vendor/assets/javascripts/raphael.js8239
-rw-r--r--vendor/assets/javascripts/timeago.js237
-rw-r--r--vendor/assets/javascripts/u2f.js4
-rw-r--r--vendor/assets/javascripts/vue-resource.full.js1318
-rw-r--r--vendor/assets/javascripts/vue-resource.js.erb2
-rw-r--r--vendor/assets/javascripts/vue-resource.min.js7
-rw-r--r--vendor/assets/javascripts/vue.full.js7515
-rw-r--r--vendor/assets/javascripts/vue.js.erb2
-rw-r--r--vendor/assets/javascripts/vue.min.js7
-rw-r--r--vendor/assets/javascripts/xterm/fit.js4
-rw-r--r--vendor/gitignore/Android.gitignore9
-rw-r--r--vendor/gitignore/CMake.gitignore1
-rw-r--r--vendor/gitignore/CodeIgniter.gitignore6
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore5
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore25
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore3
-rw-r--r--vendor/gitignore/Global/SBT.gitignore3
-rw-r--r--vendor/gitignore/Global/Stata.gitignore24
-rw-r--r--vendor/gitignore/Go.gitignore30
-rw-r--r--vendor/gitignore/Java.gitignore7
-rw-r--r--vendor/gitignore/Joomla.gitignore27
-rw-r--r--vendor/gitignore/KiCad.gitignore3
-rw-r--r--vendor/gitignore/Laravel.gitignore4
-rw-r--r--vendor/gitignore/Magento.gitignore120
-rw-r--r--vendor/gitignore/Maven.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore15
-rw-r--r--vendor/gitignore/Objective-C.gitignore7
-rw-r--r--vendor/gitignore/Perl.gitignore1
-rw-r--r--vendor/gitignore/PlayFramework.gitignore1
-rw-r--r--vendor/gitignore/PureScript.gitignore8
-rw-r--r--vendor/gitignore/Python.gitignore5
-rw-r--r--vendor/gitignore/Scala.gitignore19
-rw-r--r--vendor/gitignore/Swift.gitignore6
-rw-r--r--vendor/gitignore/Symfony.gitignore4
-rw-r--r--vendor/gitignore/TeX.gitignore9
-rw-r--r--vendor/gitignore/Unity.gitignore4
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore19
-rw-r--r--vendor/gitignore/Waf.gitignore13
-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.yml21
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml21
-rw-r--r--vendor/licenses.csv945
-rw-r--r--yarn.lock4650
5301 files changed, 119149 insertions, 75271 deletions
diff --git a/.eslintignore b/.eslintignore
index b4bfa5a1f7a..c742b08c005 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,7 +1,9 @@
+/builds/
/coverage/
/coverage-javascript/
/node_modules/
/public/
/tmp/
/vendor/
-/builds/
+karma.config.js
+webpack.config.js
diff --git a/.eslintrc b/.eslintrc
index 9ab0145820d..b0ae2a31919 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,10 +12,18 @@
"localStorage": false
},
"plugins": [
- "filenames"
+ "filenames",
+ "import"
],
+ "settings": {
+ "import/resolver": {
+ "webpack": {
+ "config": "./config/webpack.config.js"
+ }
+ }
+ },
"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 44df2ba2371..fc64b0b5892 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1,3 +1,4 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
+app/policies/project_policy.rb
diff --git a/.gitignore b/.gitignore
index 0b602d613c7..680651986e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,4 @@ eslint-report.html
/builds/*
/shared/*
/.gitlab_workhorse_secret
+/webpack-report/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index deb5345d3bd..db3d25195dc 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"
@@ -107,11 +105,16 @@ setup-test-env:
<<: *dedicated-runner
stage: prepare
script:
- - bundle exec rake gitlab:assets:compile 2>/dev/null
+ - node --version
+ - yarn --version
+ - yarn install --pure-lockfile
+ - yarn check # ensure that yarn.lock matches package.json
+ - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
expire_in: 7d
paths:
+ - node_modules
- public/assets
- tmp/tests
@@ -161,64 +164,7 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
-# Execute all testing suites against Ruby 2.1
-.ruby-21: &ruby-21
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
- <<: *use-db
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
- cache:
- key: "ruby21"
- paths:
- - vendor/ruby
-
-.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
- <<: *rspec-knapsack
- <<: *dedicated-runner
- <<: *ruby-21
-
-.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
- <<: *spinach-knapsack
- <<: *dedicated-runner
- <<: *ruby-21
-
-rspec 0 20 ruby21: *rspec-knapsack-ruby21
-rspec 1 20 ruby21: *rspec-knapsack-ruby21
-rspec 2 20 ruby21: *rspec-knapsack-ruby21
-rspec 3 20 ruby21: *rspec-knapsack-ruby21
-rspec 4 20 ruby21: *rspec-knapsack-ruby21
-rspec 5 20 ruby21: *rspec-knapsack-ruby21
-rspec 6 20 ruby21: *rspec-knapsack-ruby21
-rspec 7 20 ruby21: *rspec-knapsack-ruby21
-rspec 8 20 ruby21: *rspec-knapsack-ruby21
-rspec 9 20 ruby21: *rspec-knapsack-ruby21
-rspec 10 20 ruby21: *rspec-knapsack-ruby21
-rspec 11 20 ruby21: *rspec-knapsack-ruby21
-rspec 12 20 ruby21: *rspec-knapsack-ruby21
-rspec 13 20 ruby21: *rspec-knapsack-ruby21
-rspec 14 20 ruby21: *rspec-knapsack-ruby21
-rspec 15 20 ruby21: *rspec-knapsack-ruby21
-rspec 16 20 ruby21: *rspec-knapsack-ruby21
-rspec 17 20 ruby21: *rspec-knapsack-ruby21
-rspec 18 20 ruby21: *rspec-knapsack-ruby21
-rspec 19 20 ruby21: *rspec-knapsack-ruby21
-
-spinach 0 10 ruby21: *spinach-knapsack-ruby21
-spinach 1 10 ruby21: *spinach-knapsack-ruby21
-spinach 2 10 ruby21: *spinach-knapsack-ruby21
-spinach 3 10 ruby21: *spinach-knapsack-ruby21
-spinach 4 10 ruby21: *spinach-knapsack-ruby21
-spinach 5 10 ruby21: *spinach-knapsack-ruby21
-spinach 6 10 ruby21: *spinach-knapsack-ruby21
-spinach 7 10 ruby21: *spinach-knapsack-ruby21
-spinach 8 10 ruby21: *spinach-knapsack-ruby21
-spinach 9 10 ruby21: *spinach-knapsack-ruby21
-
# Other generic tests
-
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
@@ -232,7 +178,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
script:
- bundle exec $CI_BUILD_NAME
-rubocop:
+rubocop:
<<: *ruby-static-analysis
<<: *dedicated-runner
stage: test
@@ -241,6 +187,7 @@ rubocop:
rake haml_lint: *exec
rake scss_lint: *exec
+rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
@@ -291,23 +238,40 @@ rake db:seed_fu:
paths:
- log/development.log
-teaspoon:
+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:
- vendor/ruby
- - node_modules/
+ - node_modules
stage: test
<<: *use-db
<<: *dedicated-runner
script:
- - npm install
- - npm link istanbul
- - bundle exec rake teaspoon
+ - bundle exec rake karma
artifacts:
name: coverage-javascript
expire_in: 31d
paths:
- - coverage-javascript/default/
+ - coverage-javascript/
lint-doc:
stage: test
@@ -334,7 +298,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
@@ -380,11 +344,9 @@ lint:javascript:
paths:
- node_modules/
stage: test
- image: "node:7.1"
- before_script:
- - npm install
+ before_script: []
script:
- - npm --silent run eslint
+ - yarn run eslint
lint:javascript:report:
<<: *dedicated-runner
@@ -392,12 +354,10 @@ lint:javascript:report:
paths:
- node_modules/
stage: post-test
- image: "node:7.1"
- before_script:
- - npm install
+ before_script: []
script:
- find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- - npm --silent run eslint-report || true # ignore exit code
+ - yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
expire_in: 31d
@@ -444,19 +404,22 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - teaspoon
+ - rake karma
+ - rake gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
- mkdir public/
- - mv coverage public/coverage-ruby
- - mv coverage-javascript/default/ public/coverage-javascript/
- - mv eslint-report.html public/
+ - 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.
@@ -473,3 +436,4 @@ cache gems:
- vendor/cache
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 6d7d88c6791..34c2e097ba8 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -6,13 +6,13 @@
(How one can reproduce the issue - this is very important)
-### Expected behavior
+### What is the current *bug* behavior?
-(What you should see instead)
+(What actually happens)
-### Actual behavior
+### What is the expected *correct* behavior?
-(What actually happens)
+(What you should see instead)
### Relevant logs and/or screenshots
@@ -23,23 +23,23 @@ logs, and code as it's very hard to read otherwise.)
(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)
-#### Results of GitLab application Check
+#### Results of GitLab environment info
(For installations with omnibus-gitlab package run and paste the output of:
-`sudo gitlab-rake gitlab:check SANITIZE=true`)
+`sudo gitlab-rake gitlab:env:info`)
(For installations from source run and paste the output of:
-`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
-
-(we will only investigate if the tests are passing)
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
-#### Results of GitLab environment info
+#### Results of GitLab application Check
(For installations with omnibus-gitlab package run and paste the output of:
-`sudo gitlab-rake gitlab:env:info`)
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
(For installations from source run and paste the output of:
-`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
### Possible fixes
diff --git a/.gitlab/issue_templates/Research Proposal.md b/.gitlab/issue_templates/Research Proposal.md
new file mode 100644
index 00000000000..5676656793d
--- /dev/null
+++ b/.gitlab/issue_templates/Research Proposal.md
@@ -0,0 +1,17 @@
+### Background:
+
+(Include problem, use cases, benefits, and/or goals)
+
+**What questions are you trying to answer?**
+
+**Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?**
+
+**What is the backstory of this project and how does it impact the approach?**
+
+**What do you already know about the areas you are exploring?**
+
+**What does success look like at the end of the project?**
+
+### Links / references:
+
+/label ~"UX research"
diff --git a/.rubocop.yml b/.rubocop.yml
index bf2b2d8afc2..fa1370ea1f3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 2.1
+ TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
@@ -17,21 +17,19 @@ AllCops:
# Exclude some GitLab files
Exclude:
- 'vendor/**/*'
+ - 'node_modules/**/*'
- 'db/*'
- '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 ##################################
+# Style #######################################################################
# Check indentation of private/protected visibility modifiers.
Style/AccessModifierIndentation:
@@ -54,6 +52,16 @@ 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:
+ Enabled: true
+
# Use `Array#join` instead of `Array#*`.
Style/ArrayJoin:
Enabled: true
@@ -78,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
@@ -105,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:
@@ -115,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
@@ -131,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:
@@ -155,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
@@ -195,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:
@@ -253,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.
@@ -267,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.
@@ -299,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
@@ -339,6 +390,10 @@ Style/OpMethod:
Style/ParenthesesAroundCondition:
Enabled: true
+# Checks for an obsolete RuntimeException argument in raise/fail.
+Style/RedundantException:
+ Enabled: true
+
# Checks for parentheses that seem not to serve any purpose.
Style/RedundantParentheses:
Enabled: true
@@ -397,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
@@ -433,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
@@ -468,9 +531,9 @@ Style/WhileUntilModifier:
# Use %w or %W for arrays of words.
Style/WordArray:
- Enabled: false
+ Enabled: true
-#################### Metrics ################################
+# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
# branches, and conditions.
@@ -478,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
@@ -515,23 +582,23 @@ Metrics/PerceivedComplexity:
Enabled: true
Max: 18
-
-#################### 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
+# Lint ########################################################################
# 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
@@ -569,6 +636,10 @@ Lint/ElseLayout:
Lint/EmptyEnsure:
Enabled: true
+# Checks for the presence of `when` branches without a body.
+Lint/EmptyWhen:
+ Enabled: true
+
# Align ends correctly.
Lint/EndAlignment:
Enabled: true
@@ -581,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
@@ -593,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:
@@ -611,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
@@ -640,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
@@ -648,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
@@ -678,8 +783,7 @@ Lint/UselessSetterCall:
Lint/Void:
Enabled: true
-
-##################### Performance ############################
+# Performance #################################################################
# Use `casecmp` rather than `downcase ==`.
Performance/Casecmp:
@@ -698,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
@@ -717,8 +837,18 @@ 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 ##################################
+# Rails #######################################################################
# Enables Rails cops.
Rails:
@@ -736,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
@@ -750,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:
@@ -766,12 +923,24 @@ Rails/ReadWriteAttribute:
Rails/ScopeArgs:
Enabled: true
-##################### RSpec ##################################
+# 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.
RSpec/AnyInstance:
Enabled: false
+# Check for expectations where `be(...)` can replace `eql(...)`.
+RSpec/BeEql:
+ Enabled: true
+
# Check that the first argument to the top level describe is the tested class or
# module.
RSpec/DescribeClass:
@@ -800,6 +969,10 @@ RSpec/ExampleWording:
not: does not
IgnoredWords: []
+# Checks for `expect(...)` calls containing literal values.
+RSpec/ExpectActual:
+ Enabled: true
+
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: false
@@ -815,15 +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
+# Check for repeated description strings in example groups.
+RSpec/RepeatedDescription:
+ Enabled: false
+
+# Checks for stubbed test subjects.
+RSpec/SubjectStub:
+ Enabled: false
+
# Prefer using verifying doubles over normal doubles.
RSpec/VerifiedDoubles:
Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index d581610162f..c24142c0a11 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,87 +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: 1
-Lint/EmptyWhen:
- 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:
- Enabled: false
-
-# Offense count: 7
-RSpec/BeEql:
+# Offense count: 51
+RSpec/BeforeAfterAll:
Enabled: false
# Offense count: 15
@@ -89,11 +15,11 @@ RSpec/BeEql:
RSpec/EmptyExampleGroup:
Enabled: false
-# Offense count: 24
-RSpec/ExpectActual:
+# Offense count: 1
+RSpec/ExpectOutput:
Enabled: false
-# Offense count: 58
+# Offense count: 63
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
@@ -105,154 +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:
+# Offense count: 163
+Rails/FilePath:
Enabled: false
-# Offense count: 55
-Rails/OutputSafety:
- 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:
- Enabled: false
-
-# Offense count: 346
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
-# SupportedStyles: with_first_parameter, with_fixed_indentation
-Style/AlignParameters:
+# 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: 27
+# Offense count: 7
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: always, conditionals
-Style/AndOr:
+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
@@ -264,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:
@@ -283,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
@@ -338,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_
@@ -400,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
@@ -412,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
@@ -424,35 +243,30 @@ Style/RaiseArgs:
Style/RedundantBegin:
Enabled: false
-# Offense count: 1
-# Cop supports --auto-correct.
-Style/RedundantException:
- 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
@@ -462,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
@@ -487,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
@@ -516,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
@@ -544,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
@@ -576,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 25e02b1ae1c..42e094bdfc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,258 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 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.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)
+- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752
+- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891
+- Fix wrong call to ProjectCacheWorker.perform. !8910
+- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956
+- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko)
+- Do not display deploy keys in user's own ssh keys list. !9024
+- Show merge errors in merge request widget. !9229
+- Don't delete assigned MRs/issues when user is deleted.
+- backport of EE fix !954.
+- Refresh authorizations when transferring projects.
+- Don't use backup Active Record connections for Sidekiq.
+- Check public snippets for spam.
+
+## 8.16.5 (2017-02-14)
+
+- Patch Asciidocs rendering to block XSS.
+- Fix XSS vulnerability in SVG attachments.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Patch XSS vulnerability in RDOC support.
+
+## 8.16.4 (2017-02-02)
+
+- Support non-ASCII characters in GFM autocomplete. !8729
+- Fix search bar search param encoding. !8753
+- Fix project name label's for reference in project settings. !8795
+- Fix filtering with multiple words. !8830
+- Fixed services form cancel not redirecting back the integrations settings view. !8843
+- Fix filtering usernames with multiple words. !8851
+- Improve performance of slash commands. !8876
+- Remove old project members when retrying an export.
+- Fix permalink discussion note being collapsed.
+- Add project ID index to `project_authorizations` table to optimize queries.
+- Check public snippets for spam.
+- 19164 Add settings dropdown to mobile screens.
+
## 8.16.3 (2017-01-27)
- Add caching of droplab ajax requests. !8725
@@ -159,6 +411,17 @@ entry.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
+## 8.15.7 (2017-02-15)
+
+- No changes.
+
+## 8.15.6 (2017-02-14)
+
+- Patch Asciidocs rendering to block XSS.
+- Fix XSS vulnerability in SVG attachments.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Patch XSS vulnerability in RDOC support.
+
## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176
@@ -422,6 +685,17 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237
+## 8.14.10 (2017-02-15)
+
+- No changes.
+
+## 8.14.9 (2017-02-14)
+
+- Patch Asciidocs rendering to block XSS.
+- Fix XSS vulnerability in SVG attachments.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Patch XSS vulnerability in RDOC support.
+
## 8.14.8 (2017-01-25)
- Accept environment variables from the `pre-receive` script. !7967
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d404f1b91df..1fd29fef4f0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,31 +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)
- - [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.
@@ -40,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
@@ -84,10 +94,37 @@ 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].
+## Release retrospective and kickoff
+
+### Retrospective
+
+After each release, we have a retrospective call where we discuss what went well,
+what went wrong, and what we can improve for the next release. The
+[retrospective notes] are public and you are invited to comment on them.
+If you're interested, you can even join the
+[retrospective call][retro-kickoff-call], on the first working day after the
+22nd at 6pm CET / 9am PST.
+
+### Kickoff
+
+Before working on the next release, we have a
+kickoff call to explain what we expect to ship in the next release. The
+[kickoff notes] are public and you are invited to comment on them.
+If you're interested, you can even join the [kickoff call][retro-kickoff-call],
+on the first working day after the 7th at 6pm CET / 9am PST..
+
+[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
+[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
+[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
+
## Issue tracker
To get support for your particular problem please use the
@@ -209,6 +246,21 @@ for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
+### Stewardship
+
+For issues related to the open source stewardship of GitLab,
+there is the ~"stewardship" label.
+
+This label is to be used for issues in which the stewardship of GitLab
+is a topic of discussion. For instance if GitLab Inc. is planning to remove
+features from GitLab CE to make exclusive in GitLab EE, related issues
+would be labelled with ~"stewardship".
+
+A recent example of this was the issue for
+[bringing the time tracking API to GitLab CE][time-tracking-issue].
+
+[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
@@ -251,10 +303,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.
@@ -297,13 +352,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
@@ -387,13 +460,13 @@ 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]
1. [Testing](doc/development/testing.md)
1. [JavaScript (ES6)](https://github.com/airbnb/javascript)
-1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5)
+1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
@@ -441,6 +514,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
@@ -465,3 +539,8 @@ 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
+
+[^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
new file mode 100644
index 00000000000..1d0ba9ea182
--- /dev/null
+++ b/GITLAB_PAGES_VERSION
@@ -0,0 +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 dd7c93c5a75..2f813324a35 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,13 +1,12 @@
source 'https://rubygems.org'
-gem 'rails', '4.2.7.1'
+gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
-gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -19,24 +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-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.2.0'
-gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt', '~> 1.5.6'
+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'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -48,6 +49,9 @@ gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
+# GitLab Pages
+gem 'validates_hostname', '~> 1.0.6'
+
# Browser detection
gem 'browser', '~> 2.2'
@@ -65,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'
@@ -76,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'
@@ -99,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 'asciidoctor-plantuml', '0.0.6'
-gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+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'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -198,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'
@@ -219,24 +223,25 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
+gem 'webpack-rails', '~> 0.9.9'
+gem 'rack-proxy', '~> 0.6.0'
+
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
-gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
-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'
@@ -274,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'
+ 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'
@@ -288,22 +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 'teaspoon', '~> 1.1.0'
- gem 'teaspoon-jasmine', '~> 2.2.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-commands-teaspoon', '~> 0.0.2'
+ 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
@@ -328,11 +329,9 @@ group :test do
gem 'timecop', '~> 0.8.0'
end
-gem 'newrelic_rpm', '~> 3.16'
-
gem 'octokit', '~> 4.6.2'
-gem 'mail_room', '~> 0.9.0'
+gem 'mail_room', '~> 0.9.1'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
@@ -346,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.2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3b207d19d1f..c60c045a4c2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,41 +2,40 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
- ace-rails-ap (4.1.0)
- actionmailer (4.2.7.1)
- actionpack (= 4.2.7.1)
- actionview (= 4.2.7.1)
- activejob (= 4.2.7.1)
+ ace-rails-ap (4.1.2)
+ actionmailer (4.2.8)
+ actionpack (= 4.2.8)
+ actionview (= 4.2.8)
+ activejob (= 4.2.8)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.7.1)
- actionview (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ actionpack (4.2.8)
+ actionview (= 4.2.8)
+ activesupport (= 4.2.8)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.7.1)
- activesupport (= 4.2.7.1)
+ actionview (4.2.8)
+ activesupport (= 4.2.8)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (4.2.7.1)
- activesupport (= 4.2.7.1)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (4.2.8)
+ activesupport (= 4.2.8)
globalid (>= 0.3.0)
- activemodel (4.2.7.1)
- activesupport (= 4.2.7.1)
+ activemodel (4.2.8)
+ activesupport (= 4.2.8)
builder (~> 3.1)
- activerecord (4.2.7.1)
- activemodel (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ activerecord (4.2.8)
+ activemodel (= 4.2.8)
+ activesupport (= 4.2.8)
arel (~> 6.0)
activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5)
- activesupport (4.2.7.1)
+ activesupport (4.2.8)
i18n (~> 0.7)
- json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
@@ -47,14 +46,14 @@ GEM
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.5)
- arel (6.0.3)
+ arel (6.0.4)
asana (0.4.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
- asciidoctor-plantuml (0.0.6)
+ asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
ast (2.3.0)
attr_encrypted (3.0.3)
@@ -72,10 +71,6 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- babel-source (5.8.35)
- babel-transpiler (0.7.0)
- babel-source (>= 4.0, < 6)
- execjs (~> 2.0)
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
@@ -83,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)
@@ -90,7 +86,7 @@ GEM
sass (>= 3.3.4)
brakeman (3.4.1)
browser (2.2.0)
- builder (3.2.2)
+ builder (3.2.3)
bullet (5.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
@@ -108,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)
@@ -131,7 +128,7 @@ GEM
execjs
coffee-script-source (1.10.0)
colorize (0.7.7)
- concurrent-ruby (1.0.2)
+ concurrent-ruby (1.0.4)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -171,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)
@@ -236,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)
@@ -250,6 +250,9 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
+ gitaly (0.2.1)
+ google-protobuf (~> 3.1)
+ grpc (~> 1.0)
github-linguist (4.7.6)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
@@ -266,8 +269,6 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.1)
- gitlab-turbolinks-classic (2.5.6)
- coffee-rails
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -303,6 +304,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
+ google-protobuf (3.2.0)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
@@ -311,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)
@@ -324,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
@@ -360,8 +365,8 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.2)
- i18n (0.7.0)
- ice_nine (0.11.1)
+ i18n (0.8.1)
+ ice_nine (0.11.2)
influxdb (0.2.3)
cause
json
@@ -374,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.3)
+ 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)
@@ -405,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)
@@ -415,7 +424,7 @@ GEM
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.9.0)
+ mail_room (0.9.1)
memoist (0.15.0)
method_source (0.8.2)
mime-types (2.99.3)
@@ -424,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)
@@ -434,10 +443,8 @@ 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)
+ nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
- pkg-config (~> 1.1.7)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.2.0)
@@ -449,12 +456,12 @@ 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)
omniauth-oauth2 (~> 1.1)
- omniauth-authentiq (0.2.2)
+ omniauth-authentiq (0.3.0)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
@@ -489,6 +496,8 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
+ omniauth-oauth2-generic (0.2.2)
+ omniauth-oauth2 (~> 1.0)
omniauth-saml (1.7.0)
omniauth (~> 1.3)
ruby-saml (~> 1.4)
@@ -507,10 +516,9 @@ 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)
- pkg-config (1.1.7)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -548,30 +556,32 @@ GEM
rack (>= 1.1)
rack-protection (1.5.3)
rack
+ rack-proxy (0.6.0)
+ rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.7.1)
- actionmailer (= 4.2.7.1)
- actionpack (= 4.2.7.1)
- actionview (= 4.2.7.1)
- activejob (= 4.2.7.1)
- activemodel (= 4.2.7.1)
- activerecord (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ rails (4.2.8)
+ actionmailer (= 4.2.8)
+ actionpack (= 4.2.8)
+ actionview (= 4.2.8)
+ activejob (= 4.2.8)
+ activemodel (= 4.2.8)
+ activerecord (= 4.2.8)
+ activesupport (= 4.2.8)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.7.1)
+ railties (= 4.2.8)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.7)
+ rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0)
- nokogiri (~> 1.6.0)
+ nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (4.2.7.1)
- actionpack (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ railties (4.2.8)
+ actionpack (= 4.2.8)
+ activesupport (= 4.2.8)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
@@ -584,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,18 +652,18 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rspec_profiling (0.0.4)
+ rspec_profiling (0.0.5)
activerecord
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)
@@ -665,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)
@@ -684,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)
@@ -735,20 +746,14 @@ GEM
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
- spring-commands-teaspoon (0.0.2)
- spring (>= 0.9.1)
- sprockets (3.7.0)
+ sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.2)
- babel-source (>= 5.8.11)
- babel-transpiler
- sprockets (>= 3.0.0)
- sprockets-rails (3.1.1)
+ sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sqlite3 (1.3.11)
+ sqlite3 (1.3.13)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
@@ -761,10 +766,6 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- teaspoon (1.1.5)
- railties (>= 3.2.5, < 6)
- teaspoon-jasmine (2.2.0)
- teaspoon (>= 1.0.0)
temple (0.7.7)
test_after_commit (1.1.0)
activerecord (>= 3.2)
@@ -772,9 +773,9 @@ GEM
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
- thor (0.19.1)
- thread_safe (0.3.5)
- tilt (2.0.5)
+ thor (0.19.4)
+ thread_safe (0.3.6)
+ tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tool (0.2.3)
@@ -791,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)
@@ -799,6 +800,10 @@ 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)
version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
@@ -816,6 +821,8 @@ GEM
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
+ webpack-rails (0.9.9)
+ rails (>= 3.2.0)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -841,7 +848,7 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
- asciidoctor-plantuml (= 0.0.6)
+ asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -856,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)
@@ -871,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)
@@ -883,25 +891,25 @@ 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.2.1)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
- gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
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
@@ -910,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)
@@ -918,22 +925,21 @@ 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.0)
+ mail_room (~> 0.9.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
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.2.0)
+ omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
@@ -941,6 +947,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
+ omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
@@ -955,13 +962,14 @@ DEPENDENCIES
rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
- rails (= 4.2.7.1)
+ rack-proxy (~> 0.6.0)
+ rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
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)
@@ -971,9 +979,9 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rspec_profiling
- rubocop (~> 0.46.0)
- rubocop-rspec (~> 1.9.1)
+ rspec_profiling (~> 0.0.5)
+ rubocop (~> 0.47.1)
+ rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
@@ -996,14 +1004,10 @@ DEPENDENCIES
spring (~> 1.7.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
- spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.7.0)
- sprockets-es6 (~> 0.9.2)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
- teaspoon (~> 1.1.0)
- teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
@@ -1014,12 +1018,14 @@ DEPENDENCIES
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
+ validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
+ webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.14.2
+ 1.14.5
diff --git a/LICENSE b/LICENSE
index 1dc1bdb7411..ad4f2872db5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2016 GitLab B.V.
+Copyright (c) 2011-2017 GitLab B.V.
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/PROCESS.md b/PROCESS.md
index 993d60bbba8..fead93bd4cf 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get
-their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done].
+their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done).
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
@@ -59,67 +59,85 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature Freeze
-On the 7th of each month, the stable branches for the upcoming release will
-be frozen for major changes. Merge requests may still be merged into master
-during this period. By freezing the stable branches prior to a release there's
-no need to worry about last minute merge requests potentially breaking a lot of
-things.
+After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
+Merge requests may still be merged into master during this period,
+but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
+By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
-What is considered to be a major change is determined on a case by case basis as
-this definition depends very much on the context of changes. For example, a 5
-line change might have a big impact on the entire application. Ultimately the
-decision will be made by the maintainers and the release managers.
+Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
+and security issues will be cherry-picked into the stable branch.
+Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
+These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd).
+
+If you think a merge request should go into the upcoming release even though it does not meet these requirements,
+you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
+
+1. a Release Manager
+2. an Engineering Lead
+3. an Engineering Director, the VP of Engineering, or the CTO
+
+You can find who is who on the [team page](https://about.gitlab.com/team/).
+
+Whether an exception is made is determined by weighing the benefit and urgency of the change
+(how important it is to the company that this is released _right now_ instead of in a month)
+against the potential negative impact
+(things breaking without enough time to comfortably find and fix them before the release on the 22nd).
+When in doubt, we err on the side of _not_ cherry-picking.
+
+For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
+(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set. Merge requests without a milestone and this label will
+~"Pick into Stable" set, so that release managers can find and pick them.
+Merge requests without a milestone and this label will
not be merged into any stable branches.
## Copy & paste responses
### Improperly formatted issue
-Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version
-Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Support requests and configuration questions
Thanks for your interest in GitLab. We don't use the issue tracker for support
requests and configuration questions. Please check our
-\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available
-support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
+[getting help](https://about.gitlab.com/getting-help/) page to see all of the available
+support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
for more information.
### Code format
-Please use ``` to format console output, logs, and code as it's very hard to read otherwise.
+Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise.
### Issue fixed in newer version
-Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Improperly formatted merge request
-Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
+Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
### Inactivity close of an issue
-It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Inactivity close of a merge request
-This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
+This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
### Accepting merge requests
Is there an issue on the
-\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
+[issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
similar to this? Could you please link it here?
Please be aware that new functionality that is not marked
-\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
+[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab.
### Only accepting merge requests with green tests
@@ -134,7 +152,7 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is
-still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
+still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues).
[team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
diff --git a/README.md b/README.md
index 4f85fac4a56..09e08adbb73 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
-- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
+- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
## Website
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/favicon-blue.ico b/app/assets/images/favicon-blue.ico
new file mode 100755
index 00000000000..156fcf07588
--- /dev/null
+++ b/app/assets/images/favicon-blue.ico
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 993f427c9fb..34669dd13d6 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,65 +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 */
-/* global Turbolinks */
-(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 Turbolinks.visit(location.href);
- });
- $('li.group_member').bind('ajax:success', function() {
- return Turbolinks.visit(location.href);
- });
- 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(this);
+ 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 b4a8c827d7f..a0946eb392a 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/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, 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);
+ }
+};
- window.Api = Api;
-}).call(this);
+window.Api = Api;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
deleted file mode 100644
index 4849aab50f4..00000000000
--- a/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,256 +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 */
-/* global bp */
-/* global Cookies */
-/* global Flash */
-/* global ConfirmDangerModal */
-/* global AwardsHandler */
-/* global Aside */
-
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require jquery2 */
-/*= require jquery-ui/autocomplete */
-/*= require jquery-ui/datepicker */
-/*= require jquery-ui/draggable */
-/*= require jquery-ui/effect-highlight */
-/*= require jquery-ui/sortable */
-/*= require jquery_ujs */
-/*= require jquery.endless-scroll */
-/*= require jquery.highlight */
-/*= require jquery.waitforimages */
-/*= require jquery.atwho */
-/*= require jquery.scrollTo */
-/*= require jquery.turbolinks */
-/*= require js.cookie */
-/*= require turbolinks */
-/*= require autosave */
-/*= require bootstrap/affix */
-/*= require bootstrap/alert */
-/*= require bootstrap/button */
-/*= require bootstrap/collapse */
-/*= require bootstrap/dropdown */
-/*= require bootstrap/modal */
-/*= require bootstrap/scrollspy */
-/*= require bootstrap/tab */
-/*= require bootstrap/transition */
-/*= require bootstrap/tooltip */
-/*= require bootstrap/popover */
-/*= require select2 */
-/*= require underscore */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
-/*= require shortcuts */
-/*= require shortcuts_navigation */
-/*= require shortcuts_dashboard_navigation */
-/*= require shortcuts_issuable */
-/*= require shortcuts_network */
-/*= require jquery.nicescroll */
-/*= require date.format */
-/*= require_directory ./behaviors */
-/*= require_directory ./blob */
-/*= require_directory ./templates */
-/*= require_directory ./commit */
-/*= require_directory ./extensions */
-/*= require_directory ./lib/utils */
-/*= require_directory ./u2f */
-/*= require_directory ./droplab */
-/*= require_directory . */
-/*= require fuzzaldrin-plus */
-/*= require es6-promise.auto */
-
-(function () {
- document.addEventListener('page:fetch', 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;
- }
- });
-
- $('.nav-sidebar').niceScroll({
- cursoropacitymax: '0.4',
- cursorcolor: '#FFF',
- cursorborder: '1px solid #FFF'
- });
- $('.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();
- // bind sidebar events
- new gl.Sidebar();
- });
-}).call(this);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 8438de6cdf1..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(this);
+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 b16a2c0f73a..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(this);
+ return Autosave;
+})();
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 629dc267337..8a077f0081a 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,378 +1,510 @@
-/* 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 */
-(function() {
- this.AwardsHandler = (function() {
- var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
- function AwardsHandler() {
- this.aliases = gl.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';
+
+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) {
+ const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
+ frequentlyUsedEmojis.push(emoji);
+ Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
+ };
+
+AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
+ const frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
+ return _.compact(_.uniq(frequentlyUsedEmojis));
+};
+
+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();
+ }
+ });
+};
- $emoji.on('webkitAnimationEnd animationEnd', function() {
- $(this).removeClass(className);
- });
- };
+AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
+ const safeTerm = term.toLowerCase();
- 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;
- })();
-}).call(this);
+ 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/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 7e6c44fa1cd..f7f41d55b52 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
/* global autosize */
-/*= require autosize */
+var autosize = require('vendor/autosize');
(function() {
$(function() {
@@ -25,4 +25,4 @@
autosize.update($fields);
return $fields.css('resize', 'vertical');
});
-}).call(this);
+}).call(window);
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/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 6af8f593872..fd0840fa117 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -23,4 +23,4 @@
return e.preventDefault();
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
new file mode 100644
index 00000000000..59741cc9b1a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -0,0 +1,105 @@
+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) {
+ const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+ const 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);
+ const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+ const 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_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 d4895011be7..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
@@ -74,4 +74,4 @@
return $this.tooltip('hide');
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index ccbd6b993cb..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
@@ -59,4 +59,4 @@
return hideOrShowHelpBlock($form);
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index a7181904ac9..0726c6c9636 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -21,8 +21,7 @@
// %a.js-toggle-button
// %div.js-toggle-content
//
- $('body').on('click', '.js-toggle-button', function(e) {
- e.preventDefault();
+ $('body').on('click', '.js-toggle-button', function() {
toggleContainer($(this).closest('.js-toggle-container'));
});
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
new file mode 100644
index 00000000000..ec1c018424d
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js
@@ -0,0 +1,42 @@
+/* eslint-disable no-param-reassign, comma-dangle */
+/* global Api */
+
+require('./template_selector');
+
+((global) => {
+ class BlobCiYamlSelector extends gl.TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
+ }
+
+ requestFileSuccess(file) {
+ return super.requestFileSuccess(file);
+ }
+ }
+
+ global.BlobCiYamlSelector = BlobCiYamlSelector;
+
+ class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns } = {}) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown
+ });
+ });
+ }
+ }
+
+ global.BlobCiYamlSelectors = BlobCiYamlSelectors;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
deleted file mode 100644
index d3455fa3d8c..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ /dev/null
@@ -1,41 +0,0 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-/* global Api */
-
-/*= require blob/template_selector */
-((global) => {
- class BlobCiYamlSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobCiYamlSelector = BlobCiYamlSelector;
-
- class BlobCiYamlSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- editor,
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown
- });
- });
- }
- }
-
- global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js
new file mode 100644
index 00000000000..d4f60cc6ecd
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js
@@ -0,0 +1,19 @@
+/* global Api */
+
+require('./template_selector');
+
+(() => {
+ const global = window.gl || (window.gl = {});
+
+ class BlobDockerfileSelector extends gl.TemplateSelector {
+ requestFile(query) {
+ return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
+ }
+
+ requestFileSuccess(file) {
+ return super.requestFileSuccess(file);
+ }
+ }
+
+ global.BlobDockerfileSelector = BlobDockerfileSelector;
+})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
deleted file mode 100644
index bdf95017613..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
+++ /dev/null
@@ -1,18 +0,0 @@
-/* global Api */
-/*= require blob/template_selector */
-
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobDockerfileSelector = BlobDockerfileSelector;
-})();
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 04bfe363929..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());
});
@@ -63,4 +63,4 @@
return BlobFileDropzone;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 5fd0857db29..de20eab9cd1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var 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; },
@@ -20,4 +20,4 @@
return BlobGitignoreSelector;
})(gl.TemplateSelector);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 8236457f0f1..43e5c0a5641 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -23,4 +23,4 @@
return BlobGitignoreSelectors;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 7a14eb160d0..b582052a76e 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var 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; },
@@ -25,4 +25,4 @@
return BlobLicenseSelector;
})(gl.TemplateSelector);
-}).call(this);
+}).call(window);
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/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index dfad9b2122b..0436bbb0eaf 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -2,7 +2,7 @@
/* global EditBlob */
/* global NewCommitForm */
-/*= require_tree . */
+require('./edit_blob');
(function() {
$(function() {
@@ -12,4 +12,4 @@
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 079445e8278..a1127b9e30e 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -85,4 +85,4 @@
return EditBlob;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
new file mode 100644
index 00000000000..55d13be6e5f
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -0,0 +1,151 @@
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
+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);
+ },
+ 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,
+ },
+ 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 f9766471780..00000000000
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ /dev/null
@@ -1,84 +0,0 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global Vue */
-/* global BoardService */
-
-//= require vue
-//= require vue-resource
-//= require Sortable
-//= require_tree ./models
-//= require_tree ./stores
-//= require_tree ./services
-//= require_tree ./mixins
-//= require_tree ./filters
-//= require ./components/board
-//= require ./components/board_sidebar
-//= require ./components/new_list_dropdown
-//= require ./vue_resource_interceptor
-
-$(() => {
- const $boardApp = document.getElementById('board-app');
- const Store = gl.issueBoards.BoardsStore;
-
- 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
- },
- data: {
- state: Store.state,
- loading: true,
- endpoint: $boardApp.dataset.endpoint,
- boardId: $boardApp.dataset.boardId,
- disabled: $boardApp.dataset.disabled === 'true',
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- detailIssue: Store.detail
- },
- computed: {
- detailIssueVisible () {
- return Object.keys(this.detailIssue.issue).length;
- },
- },
- created () {
- gl.boardService = new BoardService(this.endpoint, 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;
- } else if (list.type === 'backlog') {
- list.position = -1;
- }
- });
-
- this.state.lists = _.sortBy(this.state.lists, 'position');
-
- Store.addBlankState();
- this.loading = false;
- });
- }
- });
-
- gl.IssueBoardsSearch = new Vue({
- el: '#js-boards-search',
- data: {
- filters: Store.state.filters
- },
- mounted () {
- gl.issueBoards.newListDropdownInit();
- }
- });
-});
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
new file mode 100644
index 00000000000..18324de18b3
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js
@@ -0,0 +1,105 @@
+/* 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.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
deleted file mode 100644
index a32881116d5..00000000000
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ /dev/null
@@ -1,104 +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
- },
- 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.es6 b/app/assets/javascripts/boards/components/board_blank_state.js
index d76314c1892..d76314c1892 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
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..795b3cf2ec0
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -0,0 +1,69 @@
+/* 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" />
+ </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 5fc50280811..00000000000
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ /dev/null
@@ -1,79 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
-/* global Vue */
-
-(() => {
- 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',
- props: {
- list: Object,
- issue: Object,
- issueLinkBase: String,
- disabled: Boolean,
- index: Number
- },
- data () {
- return {
- showDetail: false,
- detailIssue: Store.detail
- };
- },
- computed: {
- issueDetailVisible () {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- }
- },
- methods: {
- filterByLabel (label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
- $(e.target).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();
- },
- 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;
- }
- }
- }
- }
- });
-})();
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 630fe084175..00000000000
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ /dev/null
@@ -1,119 +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,
- },
- 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 2386d3a613c..00000000000
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ /dev/null
@@ -1,63 +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;
- })
- .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 b/app/assets/javascripts/boards/components/board_sidebar.js
new file mode 100644
index 00000000000..dfc6eed785c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -0,0 +1,72 @@
+/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* global Vue */
+/* global IssuableContext */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global Sidebar */
+
+require('./sidebar/remove_issue');
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardSidebar = Vue.extend({
+ props: {
+ currentUser: Object
+ },
+ data() {
+ return {
+ detail: Store.detail,
+ issue: {},
+ list: {},
+ };
+ },
+ computed: {
+ showSidebar () {
+ return Object.keys(this.issue).length;
+ }
+ },
+ watch: {
+ detail: {
+ handler () {
+ if (this.issue.id !== this.detail.issue.id) {
+ $('.js-issue-board-sidebar', this.$el).each((i, el) => {
+ $(el).data('glDropdown').clearMenu();
+ });
+ }
+
+ this.issue = this.detail.issue;
+ this.list = this.detail.list;
+ },
+ deep: true
+ },
+ issue () {
+ if (this.showSidebar) {
+ this.$nextTick(() => {
+ $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+ $('.right-sidebar').getNiceScroll().resize();
+ });
+ }
+ }
+ },
+ methods: {
+ closeSidebar () {
+ this.detail.issue = {};
+ }
+ },
+ mounted () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new gl.DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ gl.Subscription.bindAll('.subscription');
+ },
+ components: {
+ removeBtn: gl.issueBoards.RemoveIssueBtn,
+ },
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
deleted file mode 100644
index 75dfcb66bb0..00000000000
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ /dev/null
@@ -1,65 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global Vue */
-/* global IssuableContext */
-/* global MilestoneSelect */
-/* global LabelsSelect */
-/* global Sidebar */
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardSidebar = Vue.extend({
- props: {
- currentUser: Object
- },
- data() {
- return {
- detail: Store.detail,
- issue: {}
- };
- },
- computed: {
- showSidebar () {
- return Object.keys(this.issue).length;
- }
- },
- watch: {
- detail: {
- handler () {
- if (this.issue.id !== this.detail.issue.id) {
- $('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('glDropdown').clearMenu();
- });
- }
-
- this.issue = this.detail.issue;
- },
- deep: true
- },
- issue () {
- if (this.showSidebar) {
- this.$nextTick(() => {
- $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
- $('.right-sidebar').getNiceScroll().resize();
- });
- }
- }
- },
- methods: {
- closeSidebar () {
- this.detail.issue = {};
- }
- },
- mounted () {
- new IssuableContext(this.currentUser);
- new MilestoneSelect();
- new gl.DueDateSelectors();
- new LabelsSelect();
- new Sidebar();
- gl.Subscription.bindAll('.subscription');
- }
- });
-})();
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..22a8b971ff8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -0,0 +1,111 @@
+/* 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..9538f5b69e9
--- /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>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..6de06811d94
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -0,0 +1,49 @@
+/* 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 b/app/assets/javascripts/boards/components/modal/filters/label.js
new file mode 100644
index 00000000000..4fc8f72a145
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/label.js
@@ -0,0 +1,54 @@
+/* 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 b/app/assets/javascripts/boards/components/modal/filters/milestone.js
new file mode 100644
index 00000000000..d555599d300
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js
@@ -0,0 +1,55 @@
+/* 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 b/app/assets/javascripts/boards/components/modal/filters/user.js
new file mode 100644
index 00000000000..8523028c29c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/user.js
@@ -0,0 +1,96 @@
+/* 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 b/app/assets/javascripts/boards/components/modal/footer.js
new file mode 100644
index 00000000000..1cbc422c961
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -0,0 +1,83 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+
+require('./lists_dropdown');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooter = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
+ },
+ submitText() {
+ const count = ModalStore.selectedCount();
+
+ return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ },
+ },
+ methods: {
+ addIssues() {
+ const list = this.modal.selectedList || this.state.lists[0];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.globalId);
+
+ // Post the data to the backend
+ gl.boardService.bulkUpdate(issueIds, {
+ add_label_ids: [list.label.id],
+ }).catch(() => {
+ new Flash('Failed to update issues, please try again.', 'alert');
+
+ selectedIssues.forEach((issue) => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
+ });
+ });
+
+ // Add the issues on the frontend
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
+ },
+ },
+ components: {
+ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ },
+ template: `
+ <footer
+ class="form-actions add-issues-footer">
+ <div class="pull-left">
+ <button
+ class="btn btn-success"
+ type="button"
+ :disabled="submitDisabled"
+ @click="addIssues">
+ {{ submitText }}
+ </button>
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown></lists-dropdown>
+ </div>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="toggleModal(false)">
+ Cancel
+ </button>
+ </footer>
+ `,
+ });
+})();
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..70c088f9054
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -0,0 +1,90 @@
+/* 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..f290cd13763
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -0,0 +1,163 @@
+/* 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 b/app/assets/javascripts/boards/components/modal/list.js
new file mode 100644
index 00000000000..3730c1ecaeb
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -0,0 +1,159 @@
+/* global Vue */
+/* global ListIssue */
+/* global bp */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalList = Vue.extend({
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ image: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
+
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
+
+ if (!groups[index]) {
+ groups.push([]);
+ }
+
+ groups[index].push(issue);
+ });
+
+ return groups;
+ },
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
+ && currentPage === this.page) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <section
+ class="add-issues-list add-issues-list-columns"
+ ref="list">
+ <div
+ class="empty-state add-issues-empty-state-filter text-center"
+ v-if="issuesCount > 0 && issues.length === 0">
+ <div
+ class="svg-content"
+ v-html="image">
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
+ </div>
+ </div>
+ <div
+ v-for="group in groupedIssues"
+ class="add-issues-list-column">
+ <div
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ class="card-parent">
+ <div
+ class="card"
+ :class="{ 'is-active': issue.selected }"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath">
+ </issue-card-inner>
+ <span
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ v-if="issue.selected"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
new file mode 100644
index 00000000000..3c05120a2da
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -0,0 +1,56 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[0];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+ template: `
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: selected.label.color }">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="list in state.lists"
+ v-if="list.type == 'label'">
+ <a
+ href="#"
+ role="button"
+ :class="{ 'is-active': list.id == selected.id }"
+ @click.prevent="modal.selectedList = list">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: list.label.color }">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
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..e8cb43f3503
--- /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')">
+ 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 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
new file mode 100644
index 00000000000..e74935e1cb0
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -0,0 +1,59 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.RemoveIssueBtn = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ removeIssue() {
+ const issue = this.issue;
+ const lists = issue.getLists();
+ const labelIds = lists.map(list => list.label.id);
+
+ // Post the remove data
+ gl.boardService.bulkUpdate([issue.globalId], {
+ remove_label_ids: labelIds,
+ }).catch(() => {
+ new Flash('Failed to remove issue from board, please try again.', 'alert');
+
+ lists.forEach((list) => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach((list) => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ },
+ template: `
+ <div
+ class="block list"
+ v-if="list.type !== 'done'">
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue">
+ Remove from board
+ </button>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
new file mode 100644
index 00000000000..03425bb145b
--- /dev/null
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -0,0 +1,7 @@
+/* global Vue */
+/* global dateFormat */
+
+Vue.filter('due-date', (value) => {
+ const date = new Date(value);
+ return dateFormat(date, 'mmm d, yyyy', true);
+});
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
deleted file mode 100644
index 7e192e90fe6..00000000000
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ /dev/null
@@ -1,6 +0,0 @@
-/* global Vue */
-
-Vue.filter('due-date', (value) => {
- const date = new Date(value);
- return $.datepicker.formatDate('M d, yy', date);
-});
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js
new file mode 100644
index 00000000000..d378b7d4baf
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js
@@ -0,0 +1,14 @@
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalMixins = {
+ methods: {
+ toggleModal(toggle) {
+ ModalStore.store.showAddIssuesModal = toggle;
+ },
+ changeTab(tab) {
+ ModalStore.store.activeTab = tab;
+ },
+ },
+ };
+})();
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 31531c3ee34..00000000000
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ /dev/null
@@ -1,75 +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.id = obj.iid;
- this.title = obj.title;
- this.confidential = obj.confidential;
- this.dueDate = obj.due_date;
- this.subscribed = obj.subscribed;
- this.labels = [];
-
- 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..f237567208c
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js
@@ -0,0 +1,175 @@
+/* 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) {
+ 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 3dd5f273057..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 = ['backlog', '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 ea55158306b..00000000000
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ /dev/null
@@ -1,70 +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, boardId) {
- 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`, {});
-
- 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
- });
- }
-}
-
-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..3866c6bbfc6
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -0,0 +1,129 @@
+/* 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);
+
+ 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, `?${$.param(this.state.filters)}`);
+ }
+ };
+})();
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 cdf1b09c0a4..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ /dev/null
@@ -1,125 +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);
- const backlogList = this.findList('type', 'backlog', 'backlog');
-
- list
- .save()
- .then(() => {
- // Remove any new issues from the backlog
- // as they will be visible in the new list
- list.issues.forEach(backlogList.removeIssue.bind(backlogList));
-
- 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 !== 'backlog' && 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' && listFrom.type !== 'backlog') {
- 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..15fc6c79e8d
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -0,0 +1,107 @@
+(() => {
+ 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/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
deleted file mode 100644
index f05780167bf..00000000000
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
-(function () {
- 'use strict';
-
- function simulateEvent(el, type, options) {
- var event;
- if (!el) return;
- var ownerDocument = el.ownerDocument;
-
- options = options || {};
-
- if (/^mouse/.test(type)) {
- event = ownerDocument.createEvent('MouseEvents');
- event.initMouseEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
- } else {
- event = ownerDocument.createEvent('CustomEvent');
-
- event.initCustomEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
-
- event.dataTransfer = {
- data: {},
-
- setData: function (type, val) {
- this.data[type] = val;
- },
-
- getData: function (type) {
- return this.data[type];
- }
- };
- }
-
- if (el.dispatchEvent) {
- el.dispatchEvent(event);
- } else if (el.fireEvent) {
- el.fireEvent('on' + type, event);
- }
-
- return event;
- }
-
- function getTraget(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return (
- children[target.index] ||
- children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
- );
- }
-
- function getRect(el) {
- var rect = el.getBoundingClientRect();
- var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
-
- return {
- x: rect.left,
- y: rect.top,
- cx: rect.left + width / 2,
- cy: rect.top + height / 2,
- w: width,
- h: height,
- hw: width / 2,
- wh: height / 2
- };
- }
-
- function simulateDrag(options, callback) {
- options.to.el = options.to.el || options.from.el;
-
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
- var scrollable = options.scrollable;
-
- var fromRect = getRect(fromEl);
- var toRect = getRect(toEl);
-
- var startTime = new Date().getTime();
- var duration = options.duration || 1000;
- simulateEvent(fromEl, 'mousedown', { button: 0 });
- options.ontap && options.ontap();
- window.SIMULATE_DRAG_ACTIVE = 1;
-
- var dragInterval = setInterval(function loop() {
- var progress = (new Date().getTime() - startTime) / duration;
- var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
- var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
- var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
-
- simulateEvent(overEl, 'mousemove', {
- clientX: x,
- clientY: y
- });
-
- if (progress >= 1) {
- options.ondragend && options.ondragend();
- simulateEvent(toEl, 'mouseup');
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
- }
- }, 100);
-
- return {
- target: fromEl,
- fromList: fromEl.parentNode,
- toList: toEl.parentNode
- };
- }
-
- // Export
- window.simulateEvent = simulateEvent;
- window.simulateDrag = simulateDrag;
-})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
deleted file mode 100644
index 54c2b4ad369..00000000000
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
-/* global Vue */
-
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next(function (response) {
- Vue.activeResources -= 1;
- });
-});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index eae062a3aa3..2c1f988d987 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,71 +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;
- // 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(this);
+window.Breakpoints = Breakpoints;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index dbdadc73c3f..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(this);
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0df84234520..6efd26ccc37 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,295 +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 */
-/* global Turbolinks */
-
-(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);
- }
- }
- Build.prototype.initSidebar = function() {
- this.$sidebar = $('.js-build-sidebar');
- this.sidebarTranslationLimits = {
- min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- };
- this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
- this.$sidebar.css({
- top: this.sidebarTranslationLimits.max
- });
- this.$sidebar.niceScroll();
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
- this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
- };
-
- 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());
+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();
+ }
+ 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 Turbolinks.visit(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.translateSidebar = function(e) {
- var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
- if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
- this.$sidebar.css({
- top: newPosition
- });
- };
-
- 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: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
- });
- };
-
- return Build;
- })();
-}).call(this);
+ 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 083448552b6..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(this);
+ 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 c656ae4e241..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(this);
+ return Commit;
+})();
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 184b4561d2e..ee087c978dd 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -11,4 +11,4 @@
return CommitFile;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index f09a6b1e676..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);
}
});
};
@@ -173,4 +208,4 @@
return ImageFile;
})();
-}).call(this);
+}).call(window);
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..b5a988df897
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -0,0 +1,29 @@
+/* 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 b/app/assets/javascripts/commit/pipelines/pipelines_service.js
new file mode 100644
index 00000000000..8ae98f9bf97
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js
@@ -0,0 +1,44 @@
+/* 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 b/app/assets/javascripts/commit/pipelines/pipelines_store.js
new file mode 100644
index 00000000000..f1b80e45444
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js
@@ -0,0 +1,48 @@
+/* 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..631ed34851c
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -0,0 +1,104 @@
+/* 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 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() {
+ const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+
+ this.isLoading = true;
+ return pipelinesService.all()
+ .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.', '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"/>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index c6fdfbcaa10..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(this);
+ 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 9591df70e9c..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(this);
+ return Compare;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
new file mode 100644
index 00000000000..d91bfb1ccbd
--- /dev/null
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -0,0 +1,67 @@
+/* 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')
+ }
+ }).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;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
deleted file mode 100644
index 3587431ab69..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(this);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 35d98492012..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(this);
+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..0fb7bde1fd6
--- /dev/null
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -0,0 +1,361 @@
+/* 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.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_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
deleted file mode 100644
index b94125a4210..00000000000
--- a/app/assets/javascripts/copy_as_gfm.js.es6
+++ /dev/null
@@ -1,355 +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: {
- '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 3485f8f91ed..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 */
-
-/*= require 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(this);
+
+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 947c129d5b5..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 = label.message.map(function (value, key) {
- return key + " " + value[0];
- }).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 513298ba4e7..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
+++ /dev/null
@@ -1,44 +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 }}
- </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 2f810a69758..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ /dev/null
@@ -1,125 +0,0 @@
-/* global Vue */
-/* global Cookies */
-/* global Flash */
-
-//= require vue
-//= require_tree ./svg
-//= require_tree .
-
-$(() => {
- 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([]);
- cycleAnalyticsStore.setActiveStage(stage);
-
- cycleAnalyticsService
- .fetchStageData({
- stage,
- startDate: this.startDate,
- })
- .done((response) => {
- this.isEmptyStage = !response.events.length;
- cycleAnalyticsStore.setStageEvents(response.events);
- })
- .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 be732971c7f..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
+++ /dev/null
@@ -1,94 +0,0 @@
-/* eslint-disable no-param-reassign */
-((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 stageName = item.title.toLowerCase();
- item.active = false;
- item.isUserAllowed = data.permissions[stageName];
- item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
- item.component = `stage-${stageName}-component`;
- });
- 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) {
- this.state.events = this.decorateEvents(events);
- },
- decorateEvents(events) {
- const newEvents = [];
-
- events.forEach((item) => {
- if (!item) return;
-
- item.totalTime = item.total_time;
- item.author.webUrl = item.author.web_url;
- item.author.avatarUrl = item.author.avatar_url;
-
- if (item.created_at) item.createdAt = item.created_at;
- if (item.short_sha) item.shortSha = item.short_sha;
- if (item.commit_url) item.commitUrl = item.commit_url;
-
- delete item.author.web_url;
- delete item.author.avatar_url;
- delete item.total_time;
- delete item.created_at;
- delete item.short_sha;
- delete item.commit_url;
-
- newEvents.push(item);
- });
-
- 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 b/app/assets/javascripts/cycle_analytics/default_event_objects.js
new file mode 100644
index 00000000000..cfaf9835bf8
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js
@@ -0,0 +1,98 @@
+module.exports = {
+ issue: {
+ created_at: '',
+ url: '',
+ iid: '',
+ title: '',
+ total_time: {},
+ author: {
+ avatar_url: '',
+ id: '',
+ name: '',
+ web_url: '',
+ },
+ },
+ plan: {
+ title: '',
+ commit_url: '',
+ short_sha: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ code: {
+ title: '',
+ iid: '',
+ created_at: '',
+ url: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ test: {
+ name: '',
+ id: '',
+ date: '',
+ url: '',
+ short_sha: '',
+ commit_url: '',
+ total_time: {},
+ branch: {
+ name: '',
+ url: '',
+ },
+ },
+ review: {
+ title: '',
+ iid: '',
+ created_at: '',
+ url: '',
+ state: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ staging: {
+ id: '',
+ short_sha: '',
+ date: '',
+ url: '',
+ commit_url: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ branch: {
+ name: '',
+ url: '',
+ },
+ },
+ production: {
+ title: '',
+ created_at: '',
+ url: '',
+ iid: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+};
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 35a029194d0..00000000000
--- a/app/assets/javascripts/diff.js.es6
+++ /dev/null
@@ -1,123 +0,0 @@
-/* eslint-disable class-methods-use-this */
-
-//= require lib/utils/url_utility */
-
-(() => {
- const UNFOLD_COUNT = 20;
-
- 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');
- }
-
- $(document)
- .off('click', '.js-unfold, .diff-line-num a')
- .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
-
- 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 = $('.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 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
new file mode 100644
index 00000000000..d948dff58ec
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -0,0 +1,60 @@
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
+/* global CommentsStore */
+const Vue = require('vue');
+
+(() => {
+ const CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ },
+ data() {
+ return {
+ textareaIsEmpty: true,
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
+ } else {
+ return "Comment & unresolve discussion";
+ }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
+ } else {
+ return "Comment & resolve discussion";
+ }
+ }
+ }
+ },
+ created() {
+ this.discussion = CommentsStore.state[this.discussionId];
+ },
+ mounted: function () {
+ const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
+
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+ });
+
+ Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
+})(window);
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.es6
deleted file mode 100644
index 2514459e65e..00000000000
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ /dev/null
@@ -1,59 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
-/* global Vue */
-/* global CommentsStore */
-
-(() => {
- const CommentAndResolveBtn = Vue.extend({
- props: {
- discussionId: String,
- },
- data() {
- return {
- textareaIsEmpty: true
- };
- },
- computed: {
- discussion: function () {
- return CommentsStore.state[this.discussionId];
- },
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
- } else {
- return false;
- }
- },
- isDiscussionResolved: function () {
- return this.discussion.isResolved();
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- if (this.textareaIsEmpty) {
- return "Unresolve discussion";
- } else {
- return "Comment & unresolve discussion";
- }
- } else {
- if (this.textareaIsEmpty) {
- return "Resolve discussion";
- } else {
- return "Comment & resolve discussion";
- }
- }
- }
- },
- mounted: function () {
- const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
- this.textareaIsEmpty = $textarea.val() === '';
-
- $textarea.on('input.comment-and-resolve-btn', () => {
- this.textareaIsEmpty = $textarea.val() === '';
- });
- },
- destroyed: function () {
- $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
- }
- });
-
- Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
-})(window);
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..788daa96b3d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -0,0 +1,155 @@
+/* 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-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 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
new file mode 100644
index 00000000000..283dc330cad
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -0,0 +1,194 @@
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
+/* global DiscussionMixins */
+/* global CommentsStore */
+const Vue = require('vue');
+
+(() => {
+ const JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ discussion: {},
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
+ },
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
+ } else {
+ return this.discussionId !== this.lastResolvedId;
+ }
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
+ }
+ },
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
+ }
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector;
+ let discussionIdsInScope;
+ let firstUnresolvedDiscussionId;
+ let nextUnresolvedDiscussionId;
+ let activeTab = window.mrTabs.currentAction;
+ let hasDiscussionsToJumpTo = true;
+ let jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
+
+ const discussions = this.discussions;
+
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
+
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount += 1;
+ }
+ }
+
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
+ }
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
+
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
+
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
+ }
+ }
+
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
+ }
+ else {
+ continue;
+ }
+ }
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
+ }
+ }
+
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
+
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
+
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click');
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i += 1) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
+ }
+
+ $target = prevEl;
+ }
+ }
+
+ $.scrollTo($target, {
+ offset: 0
+ });
+ }
+ },
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
+ });
+
+ Vue.component('jump-to-discussion', JumpToDiscussion);
+})();
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.es6
deleted file mode 100644
index c3898873eaa..00000000000
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ /dev/null
@@ -1,193 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
-/* global Vue */
-/* global DiscussionMixins */
-/* global CommentsStore */
-
-(() => {
- const JumpToDiscussion = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- discussionId: String
- },
- data: function () {
- return {
- discussions: CommentsStore.state,
- };
- },
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- allResolved: function () {
- return this.unresolvedDiscussionCount === 0;
- },
- showButton: function () {
- if (this.discussionId) {
- if (this.unresolvedDiscussionCount > 1) {
- return true;
- } else {
- return this.discussionId !== this.lastResolvedId;
- }
- } else {
- return this.unresolvedDiscussionCount >= 1;
- }
- },
- lastResolvedId: function () {
- let lastId;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
-
- if (!discussion.isResolved()) {
- lastId = discussion.id;
- }
- }
- return lastId;
- }
- },
- methods: {
- jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector;
- let discussionIdsInScope;
- let firstUnresolvedDiscussionId;
- let nextUnresolvedDiscussionId;
- let activeTab = window.mrTabs.currentAction;
- let hasDiscussionsToJumpTo = true;
- let jumpToFirstDiscussion = !this.discussionId;
-
- const discussionIdsForElements = function(elements) {
- return elements.map(function() {
- return $(this).attr('data-discussion-id');
- }).toArray();
- };
-
- const discussions = this.discussions;
-
- if (activeTab === 'diffs') {
- discussionsSelector = '.diffs .notes[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
-
- let unresolvedDiscussionCount = 0;
-
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
- if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount += 1;
- }
- }
-
- if (this.discussionId && !this.discussion.isResolved()) {
- // If this is the last unresolved discussion on the diffs tab,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 1) {
- hasDiscussionsToJumpTo = false;
- }
- } else {
- // If there are no unresolved discussions on the diffs tab at all,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 0) {
- hasDiscussionsToJumpTo = false;
- }
- }
- } else if (activeTab !== 'notes') {
- // If we are on the commits or builds tabs,
- // there are no discussions to jump to.
- hasDiscussionsToJumpTo = false;
- }
-
- if (!hasDiscussionsToJumpTo) {
- // If there are no discussions to jump to on the current page,
- // switch to the notes tab and jump to the first disucssion there.
- window.mrTabs.activateTab('notes');
- activeTab = 'notes';
- jumpToFirstDiscussion = true;
- }
-
- if (activeTab === 'notes') {
- discussionsSelector = '.discussion[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
- }
-
- let currentDiscussionFound = false;
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
-
- if (!discussion) {
- // Discussions for comments on commits in this MR don't have a resolved status.
- continue;
- }
-
- if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
- firstUnresolvedDiscussionId = discussionId;
-
- if (jumpToFirstDiscussion) {
- break;
- }
- }
-
- if (!jumpToFirstDiscussion) {
- if (currentDiscussionFound) {
- if (!discussion.isResolved()) {
- nextUnresolvedDiscussionId = discussionId;
- break;
- }
- else {
- continue;
- }
- }
-
- if (discussionId === this.discussionId) {
- currentDiscussionFound = true;
- }
- }
- }
-
- nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
-
- if (!nextUnresolvedDiscussionId) {
- return;
- }
-
- let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
-
- if (activeTab === 'notes') {
- $target = $target.closest('.note-discussion');
-
- // If the next discussion is closed, toggle it open.
- if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click');
- }
- } else if (activeTab === 'diffs') {
- // Resolved discussions are hidden in the diffs tab by default.
- // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
- // When jumping between unresolved discussions on the diffs tab, we show them.
- $target.closest(".content").show();
-
- $target = $target.closest("tr.notes_holder");
- $target.show();
-
- // If we are on the diffs tab, we don't scroll to the discussion itself, but to
- // 4 diff lines above it: the line the discussion was in response to + 3 context
- let prevEl;
- for (let i = 0; i < 4; i += 1) {
- prevEl = $target.prev();
-
- // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
- if (!prevEl.hasClass("line_holder")) {
- break;
- }
-
- $target = prevEl;
- }
- }
-
- $.scrollTo($target, {
- offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
- });
- }
- }
- });
-
- Vue.component('jump-to-discussion', JumpToDiscussion);
-})();
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 5852b8bbdb7..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ /dev/null
@@ -1,113 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
-/* global Vue */
-/* global CommentsStore */
-/* global ResolveService */
-/* global Flash */
-
-(() => {
- const ResolveBtn = Vue.extend({
- props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- projectPath: String,
- canResolve: Boolean,
- resolvedBy: String
- },
- data: function () {
- return {
- discussions: CommentsStore.state,
- loading: false
- };
- },
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
- }
- },
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- note: function () {
- if (this.discussion) {
- return this.discussion.getNote(this.noteId);
- } else {
- return undefined;
- }
- },
- 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.projectPath, this.noteId);
- } else {
- promise = ResolveService
- .resolve(this.projectPath, 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);
- }
- });
-
- Vue.component('resolve-btn', ResolveBtn);
-})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
new file mode 100644
index 00000000000..de9367f2136
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -0,0 +1,26 @@
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
+/* global DiscussionMixins */
+/* global CommentsStore */
+const Vue = require('vue');
+
+((w) => {
+ w.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
+ }
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
deleted file mode 100644
index 72cdae812bc..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
-/* global Vue */
-/* global DiscussionMixins */
-/* global CommentsStore */
-
-((w) => {
- w.ResolveCount = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- loggedOut: Boolean
- },
- data: function () {
- return {
- discussions: CommentsStore.state
- };
- },
- computed: {
- allResolved: function () {
- return this.resolvedDiscussionCount === this.discussionCount;
- },
- resolvedCountText() {
- return this.discussionCount === 1 ? 'discussion' : 'discussions';
- }
- }
- });
-})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
new file mode 100644
index 00000000000..7c5fcd04d2d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -0,0 +1,62 @@
+/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
+/* global CommentsStore */
+/* global ResolveService */
+
+const Vue = require('vue');
+
+(() => {
+ const ResolveDiscussionBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
+ }
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
+ }
+ },
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
+ }
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
+ });
+
+ Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
+})();
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.es6
deleted file mode 100644
index ee5f62b2d9e..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ /dev/null
@@ -1,63 +0,0 @@
-/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
-/* global Vue */
-/* global CommentsStore */
-/* global ResolveService */
-
-(() => {
- const ResolveDiscussionBtn = Vue.extend({
- props: {
- discussionId: String,
- mergeRequestId: Number,
- projectPath: String,
- canResolve: Boolean,
- },
- data: function() {
- return {
- discussions: CommentsStore.state
- };
- },
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
- } else {
- return false;
- }
- },
- isDiscussionResolved: function () {
- if (this.discussion) {
- return this.discussion.isResolved();
- } else {
- return false;
- }
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- return "Unresolve discussion";
- } else {
- return "Resolve discussion";
- }
- },
- loading: function () {
- if (this.discussion) {
- return this.discussion.loading;
- } else {
- return false;
- }
- }
- },
- methods: {
- resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
- }
- },
- created: function () {
- CommentsStore.createDiscussion(this.discussionId, this.canResolve);
- }
- });
-
- Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
-})();
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 1b3a57d0962..00000000000
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new */
-/* global Vue */
-/* global ResolveCount */
-
-//= require_directory ./models
-//= require_directory ./stores
-//= require_directory ./services
-//= require_directory ./mixins
-//= require_directory ./components
-
-$(() => {
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
-
- window.gl = window.gl || {};
- window.gl.diffNoteApps = {};
-
- 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 b/app/assets/javascripts/diff_notes/services/resolve.js
new file mode 100644
index 00000000000..090c454e9e4
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -0,0 +1,81 @@
+/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
+/* global Flash */
+/* global CommentsStore */
+
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../vue_shared/vue_resource_interceptor');
+
+(() => {
+ window.gl = window.gl || {};
+
+ class ResolveServiceClass {
+ constructor(root) {
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
+ }
+
+ resolve(noteId) {
+ return this.noteResource.save({ noteId }, {});
+ }
+
+ unresolve(noteId) {
+ return this.noteResource.delete({ noteId }, {});
+ }
+
+ toggleResolveForDiscussion(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
+ let promise;
+
+ if (isResolved) {
+ promise = this.unResolveAll(mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(mergeRequestId, discussionId);
+ }
+
+ promise.then((response) => {
+ discussion.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ if (isResolved) {
+ discussion.unResolveAllNotes();
+ } else {
+ discussion.resolveAllNotes(resolved_by);
+ }
+
+ discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ }
+ });
+ }
+
+ resolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+
+ unResolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ discussion.loading = true;
+
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+ }
+
+ gl.DiffNotesResolveServiceClass = ResolveServiceClass;
+})();
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
deleted file mode 100644
index a52c476352d..00000000000
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ /dev/null
@@ -1,93 +0,0 @@
-/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
-/* global Vue */
-/* global Flash */
-/* global CommentsStore */
-
-((w) => {
- class ResolveServiceClass {
- constructor() {
- this.noteResource = Vue.resource('notes{/noteId}/resolve');
- this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
- }
-
- setCSRF() {
- Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
- }
-
- prepareRequest(root) {
- this.setCSRF();
- Vue.http.options.root = root;
- }
-
- resolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
- return this.noteResource.save({ noteId }, {});
- }
-
- unresolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
- return this.noteResource.delete({ noteId }, {});
- }
-
- toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
- const isResolved = discussion.isResolved();
- let promise;
-
- if (isResolved) {
- promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
- } else {
- promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
- }
-
- promise.then((response) => {
- discussion.loading = false;
-
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
-
- if (isResolved) {
- discussion.unResolveAllNotes();
- } else {
- discussion.resolveAllNotes(resolved_by);
- }
-
- discussion.updateHeadline(data);
- } else {
- new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
- }
- });
- }
-
- resolveAll(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
-
- this.prepareRequest(projectPath);
-
- discussion.loading = true;
-
- return this.discussionResource.save({
- mergeRequestId,
- discussionId
- }, {});
- }
-
- unResolveAll(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
-
- this.prepareRequest(projectPath);
-
- discussion.loading = true;
-
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId
- }, {});
- }
- }
-
- w.ResolveService = new ResolveServiceClass();
-})(window);
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..6d8174e199e
--- /dev/null
+++ b/app/assets/javascripts/dispatcher.js
@@ -0,0 +1,443 @@
+import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
+/* 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 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();
+ break;
+ case 'projects:commit:pipelines':
+ new 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();
+ 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 'projects:environments:metrics':
+ new PrometheusGraph();
+ 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 '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/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
deleted file mode 100644
index edec21e3b63..00000000000
--- a/app/assets/javascripts/dispatcher.js.es6
+++ /dev/null
@@ -1,378 +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 ShortcutsBlob */
-/* 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 */
-
-(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();
- }
- 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':
- 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: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',
- });
- 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();
- new ShortcutsBlob(true);
- 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:variables:index':
- 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(this);
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index c290e1a8355..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);
});
@@ -78,8 +84,9 @@ require('../window')(function(w){
},
destroy: function() {
- if (this.listTemplate) {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ 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 3d183f4ecb4..f2963a5eb19 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,219 +1,218 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
/* global Dropzone */
-/*= require preview_markdown */
+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(this);
+ 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..fdbb4644971
--- /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.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 d81d4cf8425..00000000000
--- a/app/assets/javascripts/due_date_select.js.es6
+++ /dev/null
@@ -1,181 +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 */
-
-(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();
- this.initStopPropagation();
- }
-
- initGlDropdown() {
- this.$dropdown.glDropdown({
- hidden: () => {
- this.$selectbox.hide();
- this.$value.css('display', '');
- }
- });
- }
-
- initDatePicker() {
- this.$datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- defaultDate: $("input[name='" + this.fieldName + "']").val(),
- altField: "input[name='" + this.fieldName + "']",
- onSelect: () => {
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
- this.updateIssueBoardIssue();
- } else {
- return this.saveDueDate(true);
- }
- }
- });
- }
-
- initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
- e.preventDefault();
-
- 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);
- }
- });
- }
-
- initStopPropagation() {
- $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
- return e.stopImmediatePropagation();
- });
- }
-
- 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 = $.datepicker.formatDate('M d, yy', dateObj);
- } 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').datepicker({
- dateFormat: 'yy-mm-dd'
- });
-
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
- e.preventDefault();
- const datepicker = $(e.target).siblings('.datepicker');
- $.datepicker._clearDate(datepicker);
- });
- }
-
- 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..0923ce6b550
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -0,0 +1,196 @@
+/* eslint-disable no-param-reassign, no-new */
+/* global Flash */
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from './environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import eventHub from '../event_hub';
+
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../vue_shared/components/table_pagination');
+require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
+
+export default 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,
+
+ // 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 971be04e2d2..00000000000
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ /dev/null
@@ -1,223 +0,0 @@
-/* eslint-disable no-param-reassign, no-new */
-/* global Vue */
-/* global EnvironmentsService */
-/* global Flash */
-
-//= require vue
-//= require vue-resource
-//= require_tree ../services/
-//= require ./environment_item
-
-(() => {
- window.gl = window.gl || {};
-
- gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
- props: {
- store: {
- type: Object,
- required: true,
- default: () => ({}),
- },
- },
-
- components: {
- 'environment-item': gl.environmentsList.EnvironmentItem,
- },
-
- data() {
- const environmentsData = document.querySelector('#environments-list-view').dataset;
-
- return {
- state: this.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,
- };
- },
-
- computed: {
- scope() {
- return this.$options.getQueryParameter('scope');
- },
-
- canReadEnvironmentParsed() {
- return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- canCreateEnvironmentParsed() {
- return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- gl.environmentsService = new EnvironmentsService(this.endpoint);
-
- const scope = this.$options.getQueryParameter('scope');
- if (scope) {
- this.store.storeVisibility(scope);
- }
-
- this.isLoading = true;
-
- return gl.environmentsService.all()
- .then(resp => resp.json())
- .then((json) => {
- this.store.storeEnvironments(json);
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
- },
-
- /**
- * Transforms the url parameter into an object and
- * returns the one requested.
- *
- * @param {String} param
- * @returns {String} The value of the requested parameter.
- */
- getQueryParameter(parameter) {
- return window.location.search.substring(1).split('&').reduce((acc, param) => {
- const paramSplited = param.split('=');
- acc[paramSplited[0]] = paramSplited[1];
- return acc;
- }, {})[parameter];
- },
-
- /**
- * Converts permission provided as strings to booleans.
- * @param {String} string
- * @returns {Boolean}
- */
- convertPermissionToBoolean(string) {
- return string === 'true';
- },
-
- methods: {
- toggleRow(model) {
- return this.store.toggleFolder(model.name);
- },
- },
-
- template: `
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul v-if="!isLoading" class="nav-links">
- <li v-bind:class="{ 'active': scope === undefined }">
- <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.filteredEnvironments.length > 0">
- <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">Build</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 state.filteredEnvironments"
- v-bind:model="model">
-
- <tr
- is="environment-item"
- :model="model"
- :toggleRow="toggleRow.bind(model)"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg"></tr>
-
- <tr v-if="model.isOpen && model.children && model.children.length > 0"
- is="environment-item"
- v-for="children in model.children"
- :model="children"
- :toggleRow="toggleRow.bind(children)"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg">
- </tr>
-
- </template>
- </tbody>
- </table>
- </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 81468f4d3bc..00000000000
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ /dev/null
@@ -1,49 +0,0 @@
-/*= require vue */
-/* global Vue */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.ActionsComponent = 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..a554998f52c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -0,0 +1,21 @@
+/**
+ * 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"
+ 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 6592c1b5f0f..00000000000
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ /dev/null
@@ -1,22 +0,0 @@
-/*= require vue */
-/* global Vue */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.ExternalUrlComponent = 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..93919d41c60
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -0,0 +1,516 @@
+import Timeago from 'timeago.js';
+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 '../../lib/utils/text_utility';
+import '../../vue_shared/components/commit';
+
+/**
+ * Envrionment Item Component
+ *
+ * Renders a table row for each environment.
+ */
+
+const timeagoInstance = new Timeago();
+
+export default {
+
+ 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,
+ },
+
+ 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 0e6bc3fdb2c..00000000000
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ /dev/null
@@ -1,537 +0,0 @@
-/* global Vue */
-/* global timeago */
-
-/*= require timeago */
-/*= require lib/utils/text_utility */
-/*= require vue_common_component/commit */
-/*= require ./environment_actions */
-/*= require ./environment_external_url */
-/*= require ./environment_stop */
-/*= require ./environment_rollback */
-/*= require ./environment_terminal_button */
-
-(() => {
- /**
- * Envrionment Item Component
- *
- * Used in a hierarchical structure to show folders with children
- * in a table.
- * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html)
- *
- * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539)
- * for more information.15
- */
-
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line
-
- gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
-
- components: {
- 'commit-component': gl.CommitComponent,
- 'actions-component': gl.environmentsList.ActionsComponent,
- 'external-url-component': gl.environmentsList.ExternalUrlComponent,
- 'stop-component': gl.environmentsList.StopComponent,
- 'rollback-component': gl.environmentsList.RollbackComponent,
- 'terminal-button-component': gl.environmentsList.TerminalButtonComponent,
- },
-
- props: {
- model: {
- type: Object,
- required: true,
- default: () => ({}),
- },
-
- toggleRow: {
- type: Function,
- required: false,
- },
-
- 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,
- },
-
- },
-
- data() {
- return {
- rowClass: {
- 'children-row': this.model['vue-isChildren'],
- },
- };
- },
-
- computed: {
-
- /**
- * If an item has a `children` entry it means it is a folder.
- * Folder items have different behaviours - it is possible to toggle
- * them and show their children.
- *
- * @returns {Boolean|Undefined}
- */
- isFolder() {
- return this.model.children && this.model.children.length > 0;
- },
-
- /**
- * If an item is inside a folder structure will return true.
- * Used for css purposes.
- *
- * @returns {Boolean|undefined}
- */
- isChildren() {
- return this.model['vue-isChildren'];
- },
-
- /**
- * Counts the number of environments in each folder.
- * Used to show a badge with the counter.
- *
- * @returns {Number|Undefined} The number of environments for the current folder.
- */
- childrenCounter() {
- return this.model.children && this.model.children.length;
- },
-
- /**
- * 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.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.last_deployment && this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0;
- },
-
- /**
- * Returns the value of the `stoppable?` key provided in the response.
- *
- * @returns {Boolean}
- */
- isStoppable() {
- return this.model['stoppable?'];
- },
-
- /**
- * 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.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.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable !== undefined;
- },
-
- /**
- * Human readable date.
- *
- * @returns {String}
- */
- createdDate() {
- return gl.environmentsList.timeagoInstance.format(
- this.model.last_deployment.deployable.created_at,
- );
- },
-
- /**
- * 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.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.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.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.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.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.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.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.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.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.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.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.$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.$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.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
- },
-
- /**
- * 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.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- this.model.last_deployment.iid !== undefined;
- },
- },
-
- /**
- * 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 v-bind:class="{ 'children-row': isChildren}">
- <a v-if="!isFolder"
- class="environment-name"
- :href="model.environment_path">
- {{model.name}}
- </a>
- <span v-else v-on:click="toggleRow(model)" class="folder-name">
- <span class="folder-icon">
- <i v-show="model.isOpen" class="fa fa-caret-down"></i>
- <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
- </span>
-
- <span>
- {{model.name}}
- </span>
-
- <span class="badge">
- {{childrenCounter}}
- </span>
- </span>
- </td>
-
- <td class="deployment-column">
- <span v-if="shouldRenderDeploymentID">
- {{deploymentInternalId}}
- </span>
-
- <span v-if="!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="model.last_deployment.deployable.build_path">
- {{buildName}}
- </a>
- </td>
-
- <td>
- <div v-if="!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="!isFolder && !hasLastDeploymentKey" class="commit-title">
- No deployments yet
- </p>
- </td>
-
- <td>
- <span
- v-if="!isFolder && canShowDate"
- class="environment-created-date-timeago">
- {{createdDate}}
- </span>
- </td>
-
- <td class="hidden-xs">
- <div v-if="!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="model.external_url && canReadEnvironment"
- class="inline js-external-url-container">
- <external-url-component
- :external-url="model.external_url">
- </external-url-component>
- </div>
-
- <div v-if="isStoppable && canCreateDeployment"
- class="inline js-stop-component-container">
- <stop-component
- :stop-url="model.stop_path">
- </stop-component>
- </div>
-
- <div v-if="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 b52298b4a88..00000000000
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ /dev/null
@@ -1,32 +0,0 @@
-/*= require vue */
-/* global Vue */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.RollbackComponent = 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 0a29f2f36e9..00000000000
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-/*= require vue */
-/* global Vue */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.StopComponent = 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"></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 050184ba497..00000000000
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ /dev/null
@@ -1,27 +0,0 @@
-/*= require vue */
-/* global Vue */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.TerminalButtonComponent = 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..5f07b612b91
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -0,0 +1,60 @@
+/**
+ * Render environments table.
+ */
+import EnvironmentItem from './environment_item';
+
+export default {
+ 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,
+ },
+
+ 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/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 3b003f6f661..00000000000
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ /dev/null
@@ -1,22 +0,0 @@
-//= require vue
-//= require_tree ./stores/
-//= require ./components/environment
-//= require ./vue_resource_interceptor
-
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListApp) {
- gl.EnvironmentsListApp.$destroy(true);
- }
- const Store = gl.environmentsList.EnvironmentsStore;
-
- gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
-
- propsData: {
- store: Store.create(),
- },
-
- });
-});
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_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
new file mode 100644
index 00000000000..7abcf6dbbea
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -0,0 +1,181 @@
+/* eslint-disable no-param-reassign, no-new */
+/* global Flash */
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from '../components/environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../vue_shared/components/table_pagination');
+require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
+
+export default 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}`;
+
+ 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/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
new file mode 100644
index 00000000000..76296c83d11
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -0,0 +1,16 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+
+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 575a45d9802..00000000000
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ /dev/null
@@ -1,24 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-class EnvironmentsService {
-
- constructor(root) {
- Vue.http.options.root = root;
-
- this.environments = Vue.resource(root);
-
- Vue.http.interceptors.push((request, next) => {
- // needed in order to not break the tests.
- if ($.rails) {
- request.headers['X-CSRF-Token'] = $.rails.csrfToken();
- }
- next();
- });
- }
-
- all() {
- return this.environments.get();
- }
-}
-
-window.EnvironmentsService = 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..d3fe3872c56
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -0,0 +1,89 @@
+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 9b4090100da..00000000000
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ /dev/null
@@ -1,190 +0,0 @@
-/* eslint-disable no-param-reassign */
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.EnvironmentsStore = {
- state: {},
-
- create() {
- this.state.environments = [];
- this.state.stoppedCounter = 0;
- this.state.availableCounter = 0;
- this.state.visibility = 'available';
- this.state.filteredEnvironments = [];
-
- return this;
- },
-
- /**
- * In order to display a tree view we need to modify the received
- * data in to a tree structure based on `environment_type`
- * sorted alphabetically.
- * In each children a `vue-` property will be added. This property will be
- * used to know if an item is a children mostly for css purposes. This is
- * needed because the children row is a fragment instance and therfore does
- * not accept non-prop attributes.
- *
- *
- * @example
- * it will transform this:
- * [
- * { name: "environment", environment_type: "review" },
- * { name: "environment_1", environment_type: null }
- * { name: "environment_2, environment_type: "review" }
- * ]
- * into this:
- * [
- * { name: "review", children:
- * [
- * { name: "environment", environment_type: "review", vue-isChildren: true},
- * { name: "environment_2", environment_type: "review", vue-isChildren: true}
- * ]
- * },
- * {name: "environment_1", environment_type: null}
- * ]
- *
- *
- * @param {Array} environments List of environments.
- * @returns {Array} Tree structured array with the received environments.
- */
- storeEnvironments(environments = []) {
- this.state.stoppedCounter = this.countByState(environments, 'stopped');
- this.state.availableCounter = this.countByState(environments, 'available');
-
- const environmentsTree = environments.reduce((acc, environment) => {
- if (environment.environment_type !== null) {
- const occurs = acc.filter(element => element.children &&
- element.name === environment.environment_type);
-
- environment['vue-isChildren'] = true;
-
- if (occurs.length) {
- acc[acc.indexOf(occurs[0])].children.push(environment);
- acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
- } else {
- acc.push({
- name: environment.environment_type,
- children: [environment],
- isOpen: false,
- 'vue-isChildren': environment['vue-isChildren'],
- });
- }
- } else {
- acc.push(environment);
- }
-
- return acc;
- }, []).slice().sort(this.sortByName);
-
- this.state.environments = environmentsTree;
-
- this.filterEnvironmentsByVisibility(this.state.environments);
-
- return environmentsTree;
- },
-
- storeVisibility(visibility) {
- this.state.visibility = visibility;
- },
- /**
- * Given the visibility prop provided by the url query parameter and which
- * changes according to the active tab we need to filter which environments
- * should be visible.
- *
- * The environments array is a recursive tree structure and we need to filter
- * both root level environments and children environments.
- *
- * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
- * functions work together.
- * The first one works as the filter that verifies if the given environment matches
- * the given state.
- * The second guarantees both root level and children elements are filtered as well.
- *
- * Given array of environments will return only
- * the environments that match the state stored.
- *
- * @param {Array} array
- * @return {Array}
- */
- filterEnvironmentsByVisibility(arr) {
- const filteredEnvironments = arr.map((item) => {
- if (item.children) {
- const filteredChildren = this.filterEnvironmentsByVisibility(
- item.children,
- ).filter(Boolean);
-
- if (filteredChildren.length) {
- item.children = filteredChildren;
- return item;
- }
- }
-
- return this.filterState(this.state.visibility, item);
- }).filter(Boolean);
-
- this.state.filteredEnvironments = filteredEnvironments;
- return filteredEnvironments;
- },
-
- /**
- * Given the state and the environment,
- * returns only if the environment state matches the one provided.
- *
- * @param {String} state
- * @param {Object} environment
- * @return {Object}
- */
- filterState(state, environment) {
- return environment.state === state && environment;
- },
-
- /**
- * Toggles folder open property given the environment type.
- *
- * @param {String} envType
- * @return {Array}
- */
- toggleFolder(envType) {
- const environments = this.state.environments;
-
- const environmentsCopy = environments.map((env) => {
- if (env['vue-isChildren'] && env.name === envType) {
- env.isOpen = !env.isOpen;
- }
-
- return env;
- });
-
- this.state.environments = environmentsCopy;
-
- return environmentsCopy;
- },
-
- /**
- * Given an array of environments, returns the number of environments
- * that have the given state.
- *
- * @param {Array} environments
- * @param {String} state
- * @returns {Number}
- */
- countByState(environments, state) {
- return environments.filter(env => env.state === state).length;
- },
-
- /**
- * Sorts the two objects provided by their name.
- *
- * @param {Object} a
- * @param {Object} b
- * @returns {Number}
- */
- sortByName(a, b) {
- const nameA = a.name.toUpperCase();
- const nameB = b.name.toUpperCase();
-
- return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
- },
- };
-})();
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
deleted file mode 100644
index 406bdbc1c7d..00000000000
--- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* 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); // eslint-disable-line
- }
-
- Vue.activeResources--; // eslint-disable-line
- });
-});
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 cd401277689..00000000000
--- a/app/assets/javascripts/extensions/array.js.es6
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */
-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 d3b58b2707a..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(this);
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 895a872568d..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(this);
+ });
+};
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/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
new file mode 100644
index 00000000000..38ff3fb7158
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -0,0 +1,81 @@
+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(':', ''));
+ }
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+ }
+
+ renderContent() {
+ const dropdownData = [];
+
+ [].forEach.call(this.input.closest('.filtered-search-input-container').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_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
deleted file mode 100644
index 7d297b8eee8..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ /dev/null
@@ -1,69 +0,0 @@
-/*= require filtered_search/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 = [{
- icon: 'fa-pencil',
- hint: 'author:',
- tag: '&lt;@author&gt;',
- }, {
- icon: 'fa-user',
- hint: 'assignee:',
- tag: '&lt;@assignee&gt;',
- }, {
- icon: 'fa-clock-o',
- hint: 'milestone:',
- tag: '&lt;%milestone&gt;',
- }, {
- icon: 'fa-tag',
- hint: 'label:',
- tag: '&lt;~label&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 b/app/assets/javascripts/filtered_search/dropdown_non_user.js
new file mode 100644
index 00000000000..b3dc3e502c5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -0,0 +1,44 @@
+require('./filtered_search_dropdown');
+
+/* global droplabAjax */
+/* global droplabFilter */
+
+(() => {
+ class DropdownNonUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter, endpoint, symbol) {
+ super(droplab, dropdown, input, filter);
+ this.symbol = symbol;
+ this.config = {
+ droplabAjax: {
+ endpoint,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ },
+ droplabFilter: {
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const title = selected.querySelector('.js-data-value').innerText.trim();
+ return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
+ });
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab
+ .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownNonUser = DropdownNonUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
deleted file mode 100644
index 13cbec1be4a..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
+++ /dev/null
@@ -1,44 +0,0 @@
-/*= require filtered_search/filtered_search_dropdown */
-
-/* global droplabAjax */
-/* global droplabFilter */
-
-(() => {
- class DropdownNonUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter, endpoint, symbol) {
- super(droplab, dropdown, input, filter);
- this.symbol = symbol;
- this.config = {
- droplabAjax: {
- endpoint,
- method: 'setData',
- loadingTemplate: this.loadingTemplate,
- },
- droplabFilter: {
- filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
- },
- };
- }
-
- itemClicked(e) {
- super.itemClicked(e, (selected) => {
- const title = selected.querySelector('.js-data-value').innerText.trim();
- return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
- });
- }
-
- renderContent(forceShowList = false) {
- this.droplab
- .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
- super.renderContent(forceShowList);
- }
-
- init() {
- this.droplab
- .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
- }
- }
-
- window.gl = window.gl || {};
- gl.DropdownNonUser = DropdownNonUser;
-})();
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 162fd6044e5..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ /dev/null
@@ -1,60 +0,0 @@
-/*= require filtered_search/filtered_search_dropdown */
-
-/* global droplabAjaxFilter */
-
-(() => {
- class DropdownUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- droplabAjaxFilter: {
- endpoint: '/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..a5a6b56a0d3
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -0,0 +1,174 @@
+(() => {
+ 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.getSearchInput(input);
+ let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+ lastToken = lastToken.key || lastToken || '';
+
+ if (!lastToken || searchInput.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, 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 tokens = [].slice.call(document.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 = document.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 d188718c5f3..856eb6590ee 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,7 +1,10 @@
- // This is a manifest file that'll be compiled into including all the files listed below.
- // Add new JavaScript code in separate files in this directory and they'll automatically
- // be included in the compiled file accessible from http://example.com/assets/application.js
- // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
- // the compiled file.
- //
- /*= require_tree . */
+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..134bdc6ad80
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -0,0 +1,123 @@
+(() => {
+ 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.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 859d6515531..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ /dev/null
@@ -1,112 +0,0 @@
-(() => {
- const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
-
- class FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- this.droplab = droplab;
- this.hookId = 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) {
- if (forceShowList && this.getCurrentHook().list.hidden) {
- this.getCurrentHook().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() {
- this.getCurrentHook().list.hide();
- }
-
- resetFilters() {
- const hook = this.getCurrentHook();
- 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..d37c812c1f7
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -0,0 +1,189 @@
+/* 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 = '', clicked = false) {
+ const input = document.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 - document.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 = document.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 00e1c28692f..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ /dev/null
@@ -1,207 +0,0 @@
-/* global DropLab */
-
-(() => {
- class FilteredSearchDropdownManager {
- constructor() {
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.filteredSearchInput = document.querySelector('.filtered-search');
-
- this.setupMapping();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('page:fetch', this.cleanupWrapper);
- }
-
- cleanup() {
- if (this.droplab) {
- this.droplab.destroy();
- this.droplab = null;
- }
-
- this.setupMapping();
-
- document.removeEventListener('page:fetch', 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: ['milestones.json', '%'],
- element: document.querySelector('#js-dropdown-milestone'),
- },
- label: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: ['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 = gl.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..835e87a28d7
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -0,0 +1,379 @@
+(() => {
+ class FilteredSearchManager {
+ constructor(page) {
+ this.filteredSearchInput = document.querySelector('.filtered-search');
+ this.clearSearchButton = document.querySelector('.clear-search');
+ this.tokensContainer = document.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.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.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('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.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('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) {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
+
+ this.search();
+ }
+ }
+ }
+
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
+
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
+ }
+ }
+
+ unselectEditTokens(e) {
+ const inputContainer = document.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();
+ }
+
+ 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('&')}`;
+
+ 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 029564ffc61..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ /dev/null
@@ -1,230 +0,0 @@
-/* global Turbolinks */
-
-(() => {
- class FilteredSearchManager {
- constructor() {
- this.filteredSearchInput = document.querySelector('.filtered-search');
- this.clearSearchButton = document.querySelector('.clear-search');
-
- if (this.filteredSearchInput) {
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager();
-
- this.bindEvents();
- this.loadSearchParamsFromURL();
- this.dropdownManager.setDropdown();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('page:fetch', this.cleanupWrapper);
- }
- }
-
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('page:fetch', 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 gl.FilteredSearchTokenKeys
- const condition = gl.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 = gl.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 = gl.FilteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = gl.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}`);
- }
-
- Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
- }
-
- 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.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index e6b53cd4b55..e6b53cd4b55 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
new file mode 100644
index 00000000000..9bf1b1ced88
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -0,0 +1,48 @@
+require('./filtered_search_token_keys');
+
+(() => {
+ class FilteredSearchTokenizer {
+ static processTokens(input) {
+ const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
+ // Regex extracts `(token):(symbol)(value)`
+ // Values that start with a double quote must end in a double quote (same for single)
+ const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
+ const tokens = [];
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
+ }
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
+ }
+
+ return {
+ tokens,
+ lastToken,
+ searchToken,
+ };
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
deleted file mode 100644
index cf53845a48b..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
+++ /dev/null
@@ -1,45 +0,0 @@
-(() => {
- class FilteredSearchTokenizer {
- static processTokens(input) {
- // Regex extracts `(token):(symbol)(value)`
- // Values that start with a double quote must end in a double quote (same for single)
- const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
- const tokens = [];
- let lastToken = null;
- const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
- let tokenValue = v1 || v2 || v3;
- let tokenSymbol = symbol;
-
- if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
- tokenSymbol = tokenValue;
- tokenValue = '';
- }
-
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
- return '';
- }).replace(/\s{2,}/g, ' ').trim() || '';
-
- if (tokens.length > 0) {
- const last = tokens[tokens.length - 1];
- const lastString = `${last.key}:${last.symbol}${last.value}`;
- lastToken = input.lastIndexOf(lastString) ===
- input.length - lastString.length ? last : searchToken;
- } else {
- lastToken = searchToken;
- }
-
- return {
- tokens,
- lastToken,
- searchToken,
- };
- }
- }
-
- window.gl = window.gl || {};
- gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
-})();
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..320afa26130
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -0,0 +1,200 @@
+class FilteredSearchVisualTokens {
+ static getLastVisualTokenBeforeInput() {
+ const inputLi = document.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 = document.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 = document.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 = document.querySelector('.tokens-container');
+ const input = document.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);
+ } else {
+ const previousTokenName = lastVisualToken.querySelector('.name').innerText;
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.removeChild(lastVisualToken);
+
+ const value = tokenValue || tokenName;
+ addVisualTokenElement(previousTokenName, value);
+ }
+ }
+
+ 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 = document.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 = document.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')) {
+ 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 = document.querySelector('.filtered-search');
+ const inputLi = input.parentElement;
+ const tokenContainer = document.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 249fe23d4cb..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(this);
+ 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 7f1f2a5d278..00000000000
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ /dev/null
@@ -1,380 +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] || match[1] === "") ? match[1] : match[2];
- } 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));
- });
- },
- 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(this);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d2f66cf5249..a03f1202a6d 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,844 +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 */
-/* global Turbolinks */
-(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() {
- return this.options.query(this.input.val(), function(data) {
- 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().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() {
- 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();
- return this.dropdown.trigger('shown.gl.dropdown');
- };
+ // 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();
+ }
- GitLabDropdown.prototype.positionMenuAbove = function() {
- var $button = $(this.el);
- var $menu = this.dropdown.find('.dropdown-menu');
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
- $menu.css('top', ($button.height() + $menu.height()) * -1);
- };
+ if (this.options.opened) {
+ this.options.opened.call(this, e);
+ }
- 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');
- };
+ return this.dropdown.trigger('shown.gl.dropdown');
+ };
- // 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.positionMenuAbove = function() {
+ var $button = $(this.el);
+ var $menu = this.dropdown.find('.dropdown-menu');
- for (var i = 0; i < html.length; i += 1) {
- var el = html[i];
+ $menu.css('top', ($button.height() + $menu.height()) * -1);
+ };
- if (el instanceof jQuery) {
- el = el.get(0);
- }
+ 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');
+ };
- if (typeof el === 'string') {
- ul.innerHTML += el;
- } else {
- ul.appendChild(el);
- }
+ // Render the full menu
+ GitLabDropdown.prototype.renderMenu = function(html) {
+ if (this.options.renderMenu) {
+ return this.options.renderMenu(html);
+ } else {
+ var ul = document.createElement('ul');
+
+ for (var i = 0; i < html.length; i += 1) {
+ var el = html[i];
+
+ 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 !== '#') {
- Turbolinks.visit(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(this);
+
+ 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 16be930a2f4..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 32c26349da0..a433c7ba8f0 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,12 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
+import Chart from 'vendor/Chart';
+import ContributorsStatGraph from './stat_graph_contributors';
-(function() {
-
-}).call(this);
+// 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 2e6da5750de..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(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 73715286c4a..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 */
-/*= 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(this);
+ 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 cacfc177fc8..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 */
-
-/*= 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(this);
+ 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 29c3163328f..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(this);
+ }
+};
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 10dfd05fe3c..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(this);
+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/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 bc88dc2d092..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(this);
+ return GroupsSelect;
+})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index fa85f9a6c86..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.addDelimiter(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/importer_status.js b/app/assets/javascripts/importer_status.js
index 9390136d3d8..34e4a257ff9 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -78,4 +78,4 @@
new window.ImporterStatus(jobsImportPath, importPath);
}
});
-}).call(this);
+}).call(window);
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 f63d700fd65..00000000000
--- a/app/assets/javascripts/issuable.js.es6
+++ /dev/null
@@ -1,189 +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 Turbolinks */
-
-((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 Turbolinks.visit(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);
- Turbolinks.visit(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 b/app/assets/javascripts/issuable/issuable_bundle.js
new file mode 100644
index 00000000000..e927cc0077c
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_bundle.js
@@ -0,0 +1 @@
+require('./time_tracking/time_tracking_bundle');
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6
deleted file mode 100644
index 7d0465aa8b4..00000000000
--- a/app/assets/javascripts/issuable/issuable_bundle.js.es6
+++ /dev/null
@@ -1 +0,0 @@
-//= require ./time_tracking/time_tracking_bundle
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 72433df2818..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
+++ /dev/null
@@ -1,42 +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 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
new file mode 100644
index 00000000000..750468c679b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
@@ -0,0 +1,69 @@
+/* global Vue */
+require('../../../lib/utils/pretty_time');
+
+(() => {
+ const prettyTime = gl.utils.prettyTime;
+
+ Vue.component('time-tracking-comparison-pane', {
+ name: 'time-tracking-comparison-pane',
+ props: [
+ 'timeSpent',
+ 'timeEstimate',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ ],
+ computed: {
+ parsedRemaining() {
+ const diffSeconds = this.timeEstimate - this.timeSpent;
+ return prettyTime.parseSeconds(diffSeconds);
+ },
+ timeRemainingHumanReadable() {
+ return prettyTime.stringifyTime(this.parsedRemaining);
+ },
+ timeRemainingTooltip() {
+ const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
+ return `${prefix} ${this.timeRemainingHumanReadable}`;
+ },
+ /* Diff values for comparison meter */
+ timeRemainingMinutes() {
+ return this.timeEstimate - this.timeSpent;
+ },
+ timeRemainingPercent() {
+ return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
+ },
+ timeRemainingStatusClass() {
+ return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
+ },
+ /* Parsed time values */
+ parsedEstimate() {
+ return prettyTime.parseSeconds(this.timeEstimate);
+ },
+ parsedSpent() {
+ return prettyTime.parseSeconds(this.timeSpent);
+ },
+ },
+ template: `
+ <div class='time-tracking-comparison-pane'>
+ <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
+ :aria-valuenow='timeRemainingTooltip'
+ :title='timeRemainingTooltip'
+ :data-original-title='timeRemainingTooltip'
+ :class='timeRemainingStatusClass'>
+ <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
+ <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
+ </div>
+ <div class='compare-display-container'>
+ <div class='compare-display pull-left'>
+ <span class='compare-label'>Spent</span>
+ <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
+ </div>
+ <div class='compare-display estimated pull-right'>
+ <span class='compare-label'>Est</span>
+ <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
+ </div>
+ </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.es6
deleted file mode 100644
index 6abbd5dd167..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
+++ /dev/null
@@ -1,69 +0,0 @@
-/* global Vue */
-//= require lib/utils/pretty_time
-
-(() => {
- const prettyTime = gl.utils.prettyTime;
-
- Vue.component('time-tracking-comparison-pane', {
- name: 'time-tracking-comparison-pane',
- props: [
- 'timeSpent',
- 'timeEstimate',
- 'timeSpentHumanReadable',
- 'timeEstimateHumanReadable',
- ],
- computed: {
- parsedRemaining() {
- const diffSeconds = this.timeEstimate - this.timeSpent;
- return prettyTime.parseSeconds(diffSeconds);
- },
- timeRemainingHumanReadable() {
- return prettyTime.stringifyTime(this.parsedRemaining);
- },
- timeRemainingTooltip() {
- const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
- return `${prefix} ${this.timeRemainingHumanReadable}`;
- },
- /* Diff values for comparison meter */
- timeRemainingMinutes() {
- return this.timeEstimate - this.timeSpent;
- },
- timeRemainingPercent() {
- return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
- },
- timeRemainingStatusClass() {
- return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
- },
- /* Parsed time values */
- parsedEstimate() {
- return prettyTime.parseSeconds(this.timeEstimate);
- },
- parsedSpent() {
- return prettyTime.parseSeconds(this.timeSpent);
- },
- },
- template: `
- <div class='time-tracking-comparison-pane'>
- <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
- :aria-valuenow='timeRemainingTooltip'
- :title='timeRemainingTooltip'
- :data-original-title='timeRemainingTooltip'
- :class='timeRemainingStatusClass'>
- <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
- <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
- </div>
- <div class='compare-display-container'>
- <div class='compare-display pull-left'>
- <span class='compare-label'>Spent</span>
- <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
- </div>
- <div class='compare-display estimated pull-right'>
- <span class='compare-label'>Est</span>
- <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
- </div>
- </div>
- </div>
- </div>
- `,
- });
-})();
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 26563a7713b..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
+++ /dev/null
@@ -1,118 +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 0b8da2b1f4f..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
+++ /dev/null
@@ -1,61 +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/issuable_context.js b/app/assets/javascripts/issuable_context.js
index c77fbb6a1c7..115312d4b83 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -76,4 +76,4 @@
return IssuableContext;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 293b856dc4d..de184ab2675 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -3,6 +3,8 @@
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
+/* global dateFormat */
+/* global Pikaday */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -13,7 +15,7 @@
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
- var $issuableDueDate;
+ var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = bind(this.toggleWip, this);
this.renderWipExplanation = bind(this.renderWipExplanation, this);
@@ -35,12 +37,15 @@
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
- $('.datepicker').datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: function(dateText, inst) {
- return $issuableDueDate.val(dateText);
+ calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ onSelect: function(dateText) {
+ $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()));
+ });
+ calendar.setDate(new Date($issuableDueDate.val()));
}
}
@@ -151,4 +156,4 @@
return IssuableForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 081b0d8b0d7..ef4029a8623 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,155 +1,129 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
-/*= require flash */
-/*= require jquery.waitforimages */
-/*= require task_list */
+require('./flash');
+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);
- // Prevent duplicate event bindings
- this.disableTaskList();
- if ($('a.btn-close').length) {
- this.initTaskList();
- this.initIssueBtnEventListeners();
- }
- this.initMergeRequests();
- this.initRelatedBranches();
- this.initCanCreateBranch();
- }
-
- Issue.prototype.initTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('enable');
- return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
- };
-
- 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'));
+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;
}
- $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');
- if (isClose) {
- $('a.btn-close').addClass('hidden');
- $('a.btn-reopen').removeClass('hidden');
- $('div.status-box-closed').removeClass('hidden');
- $('div.status-box-open').addClass('hidden');
- } else {
- $('a.btn-reopen').addClass('hidden');
- $('a.btn-close').removeClass('hidden');
- $('div.status-box-closed').addClass('hidden');
- $('div.status-box-open').removeClass('hidden');
- }
- } else {
- new Flash(issueFailMessage, 'alert');
- }
- return $this.prop('disabled', false);
- }
- });
});
- };
+ Issue.initIssueBtnEventListeners();
+ }
+ Issue.initMergeRequests();
+ Issue.initRelatedBranches();
+ Issue.initCanCreateBranch();
+ }
- Issue.prototype.submitNoteForm = function(form) {
- var noteText;
- noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
- return form.submit();
+ 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'));
}
- };
-
- Issue.prototype.disableTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
- };
-
- Issue.prototype.updateTaskList = function() {
- var patchData;
- patchData = {};
- patchData['issue'] = {
- 'description': $('.js-task-list-field', this).val()
- };
+ $this.prop('disabled', true);
+ url = $this.attr('href');
return $.ajax({
- type: 'PATCH',
- url: $('form.js-issuable-update').attr('action'),
- data: patchData,
- success: function(issue) {
- document.querySelector('#task_status').innerText = issue.task_status;
- document.querySelector('#task_status_short').innerText = issue.task_status_short;
+ 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);
+ }
+ } else {
+ new Flash(issueFailMessage, 'alert');
+ }
+ return $this.prop('disabled', false);
}
});
- // TODO (rspeicher): Make the issue description inline-editable like a note so
- // that we can re-use its form here
- };
+ });
+ }
- 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 submitNoteForm(form) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ return form.submit();
+ }
+ }
- 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 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.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 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);
}
- 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();
- }
- });
- };
+ });
+ }
+
+ 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 Issue;
- })();
-}).call(this);
+export default Issue;
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 1d6eff11403..b2cfd3ef2a3 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -31,4 +31,4 @@
return IssueStatusSelect;
})();
-}).call(this);
+}).call(window);
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 b/app/assets/javascripts/label_manager.js
new file mode 100644
index 00000000000..38b2eb9ff14
--- /dev/null
+++ b/app/assets/javascripts/label_manager.js
@@ -0,0 +1,118 @@
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
+/* global Flash */
+/* global Sortable */
+
+((global) => {
+ class LabelManager {
+ constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
+ this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
+ this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
+ this.otherLabels = otherLabels || $('.js-other-labels');
+ this.errorMessage = 'Unable to update label prioritization at this time';
+ this.emptyState = document.querySelector('#js-priority-labels-empty-state');
+ this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
+ filter: '.empty-message',
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ dataIdAttr: 'data-id',
+ onUpdate: this.onPrioritySortUpdate.bind(this),
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+ }
+
+ onTogglePriorityClick(e) {
+ e.preventDefault();
+ const _this = e.data;
+ const $btn = $(e.currentTarget);
+ const $label = $(`#${$btn.data('domId')}`);
+ const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
+ $tooltip.tooltip('destroy');
+ _this.toggleLabelPriority($label, action);
+ _this.toggleEmptyState($label, $btn, action);
+ }
+
+ toggleEmptyState($label, $btn, action) {
+ this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
+ }
+
+ toggleLabelPriority($label, action, persistState) {
+ if (persistState == null) {
+ persistState = true;
+ }
+ let xhr;
+ const _this = this;
+ const url = $label.find('.js-toggle-priority').data('url');
+ let $target = this.prioritizedLabels;
+ let $from = this.otherLabels;
+ if (action === 'remove') {
+ $target = this.otherLabels;
+ $from = this.prioritizedLabels;
+ }
+ $label.detach().appendTo($target);
+ if ($from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ if ($target.find('> li:not(.empty-message)').length) {
+ $target.find('.empty-message').addClass('hidden');
+ }
+ // Return if we are not persisting state
+ if (!persistState) {
+ return;
+ }
+ if (action === 'remove') {
+ xhr = $.ajax({
+ url,
+ type: 'DELETE'
+ });
+ // Restore empty message
+ if (!$from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ } else {
+ xhr = this.savePrioritySort($label, action);
+ }
+ return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+ }
+
+ onPrioritySortUpdate() {
+ const xhr = this.savePrioritySort();
+ return xhr.fail(function() {
+ return new Flash(this.errorMessage, 'alert');
+ });
+ }
+
+ savePrioritySort() {
+ return $.post({
+ url: this.prioritizedLabels.data('url'),
+ data: {
+ label_ids: this.getSortedLabelsIds()
+ }
+ });
+ }
+
+ rollbackLabelPosition($label, originalAction) {
+ const action = originalAction === 'remove' ? 'add' : 'remove';
+ this.toggleLabelPriority($label, action, false);
+ return new Flash(this.errorMessage, 'alert');
+ }
+
+ getSortedLabelsIds() {
+ const sortedIds = [];
+ this.prioritizedLabels.find('> li').each(function() {
+ const id = $(this).data('id');
+
+ if (id) {
+ sortedIds.push(id);
+ }
+ });
+ return sortedIds;
+ }
+ }
+
+ gl.LabelManager = LabelManager;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
deleted file mode 100644
index 2a50b72c8aa..00000000000
--- a/app/assets/javascripts/label_manager.js.es6
+++ /dev/null
@@ -1,112 +0,0 @@
-/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
-/* global Flash */
-
-((global) => {
- class LabelManager {
- constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
- this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
- this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
- this.otherLabels = otherLabels || $('.js-other-labels');
- this.errorMessage = 'Unable to update label prioritization at this time';
- this.emptyState = document.querySelector('#js-priority-labels-empty-state');
- this.prioritizedLabels.sortable({
- items: 'li',
- placeholder: 'list-placeholder',
- axis: 'y',
- update: this.onPrioritySortUpdate.bind(this)
- });
- this.bindEvents();
- }
-
- bindEvents() {
- return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
- }
-
- onTogglePriorityClick(e) {
- e.preventDefault();
- const _this = e.data;
- const $btn = $(e.currentTarget);
- const $label = $(`#${$btn.data('domId')}`);
- const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
- const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
- $tooltip.tooltip('destroy');
- _this.toggleLabelPriority($label, action);
- _this.toggleEmptyState($label, $btn, action);
- }
-
- toggleEmptyState($label, $btn, action) {
- this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
- }
-
- toggleLabelPriority($label, action, persistState) {
- if (persistState == null) {
- persistState = true;
- }
- let xhr;
- const _this = this;
- const url = $label.find('.js-toggle-priority').data('url');
- let $target = this.prioritizedLabels;
- let $from = this.otherLabels;
- if (action === 'remove') {
- $target = this.otherLabels;
- $from = this.prioritizedLabels;
- }
- if ($from.find('li').length === 1) {
- $from.find('.empty-message').removeClass('hidden');
- }
- if (!$target.find('li').length) {
- $target.find('.empty-message').addClass('hidden');
- }
- $label.detach().appendTo($target);
- // Return if we are not persisting state
- if (!persistState) {
- return;
- }
- if (action === 'remove') {
- xhr = $.ajax({
- url,
- type: 'DELETE'
- });
- // Restore empty message
- if (!$from.find('li').length) {
- $from.find('.empty-message').removeClass('hidden');
- }
- } else {
- xhr = this.savePrioritySort($label, action);
- }
- return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
- }
-
- onPrioritySortUpdate() {
- const xhr = this.savePrioritySort();
- return xhr.fail(function() {
- return new Flash(this.errorMessage, 'alert');
- });
- }
-
- savePrioritySort() {
- return $.post({
- url: this.prioritizedLabels.data('url'),
- data: {
- label_ids: this.getSortedLabelsIds()
- }
- });
- }
-
- rollbackLabelPosition($label, originalAction) {
- const action = originalAction === 'remove' ? 'add' : 'remove';
- this.toggleLabelPriority($label, action, false);
- return new Flash(this.errorMessage, 'alert');
- }
-
- getSortedLabelsIds() {
- const sortedIds = [];
- this.prioritizedLabels.find('li').each(function() {
- sortedIds.push($(this).data('id'));
- });
- return sortedIds;
- }
- }
-
- gl.LabelManager = LabelManager;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 40ad6fc348e..17a3fc1b1e4 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -43,4 +43,4 @@
return Labels;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 70dc0d06b7b..9e2d14c7f87 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,10 +4,17 @@
(function() {
this.LabelsSelect = (function() {
- function LabelsSelect() {
- var _this;
+ function LabelsSelect(els) {
+ var _this, $els;
_this = this;
- $('.js-label-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-label-select');
+ }
+
+ $els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
@@ -324,7 +331,7 @@
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) {
- var isIssueIndex, isMRIndex, page;
+ var isIssueIndex, isMRIndex, page, boardsModel;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -346,22 +353,31 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ 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) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
+
+ if (boardsModel) {
if (label.isAny) {
- gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ boardsModel['label_name'] = [];
}
else if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ boardsModel['label_name'].push(label.title);
}
else {
- var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ var filters = boardsModel['label_name'];
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
});
- gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ boardsModel['label_name'] = filters;
}
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+ if (!$dropdown.closest('.add-issues-modal').length) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
e.preventDefault();
return;
}
@@ -488,4 +504,4 @@
return LabelsSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 1c0ea317c1a..08ca9e4fa4d 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -44,4 +44,4 @@
}
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
index 25e623f0fdc..976769ba84a 100644
--- a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
+++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
@@ -7,19 +7,28 @@ ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort.
File.basename(file, '.js').sub(/^mode-/, '')
end
%>
-
+// Lazy-load configuration when ace.edit is called
(function() {
- window.gon = window.gon || {};
- var basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
- ace.config.set('basePath', basePath);
+ var basePath;
+ var ace = window.ace;
+ var edit = ace.edit;
+ ace.edit = function() {
+ window.gon = window.gon || {};
+ basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
+ ace.config.set('basePath', basePath);
- // configure paths for all worker modules
+ // configure paths for all worker modules
<% ace_workers.each do |worker| %>
- ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/worker-<%= worker %>.js');
+ ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= File.basename(asset_path("ace/worker-#{worker}.js")) %>');
<% end %>
- // configure paths for all mode modules
+ // configure paths for all mode modules
<% ace_modes.each do |mode| %>
- ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/mode-<%= mode %>.js');
+ ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= File.basename(asset_path("ace/mode-#{mode}.js")) %>');
<% end %>
+
+ // restore original method
+ ace.edit = edit;
+ return ace.edit.apply(ace, arguments);
+ };
})();
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
deleted file mode 100644
index d8ad5aaeffe..00000000000
--- a/app/assets/javascripts/lib/chart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require Chart */
-
-(function() {
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
deleted file mode 100644
index 5221f85ba7a..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(this);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
deleted file mode 100644
index 57e7986576c..00000000000
--- a/app/assets/javascripts/lib/d3.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require d3 */
-
-(function() {
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
deleted file mode 100644
index 5a9a501efe3..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(this);
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index ce090a2e4fd..d93c1d0da59 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -46,4 +46,4 @@
return dfd.promise();
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
new file mode 100644
index 00000000000..2955bda1a36
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
@@ -0,0 +1,112 @@
+/**
+ * Linked Tabs
+ *
+ * Handles persisting and restores the current tab selection and content.
+ * Reusable component for static content.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links tab-links">
+ * <li class="active">
+ * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1">
+ * Tab 1
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2">
+ * Tab 2
+ * </a>
+ * </li>
+ *
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="tab1">
+ * Tab 1 Content
+ * </div>
+ * <div class="tab-pane" id="tab2">
+ * Tab 2 Content
+ * </div>
+ * </div>
+ *
+ *
+ * ### How to use
+ *
+ * new window.gl.LinkedTabs({
+ * action: "#{controller.action_name}",
+ * defaultAction: 'tab1',
+ * parentEl: '.tab-links'
+ * });
+ */
+
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.LinkedTabs = class LinkedTabs {
+ /**
+ * Binds the events and activates de default tab.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options || {};
+
+ this.defaultAction = this.options.defaultAction;
+ this.action = this.options.action || this.defaultAction;
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.currentLocation = window.location;
+
+ const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
+
+ // since this is a custom event we need jQuery :(
+ $(document)
+ .off('shown.bs.tab', tabSelector)
+ .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
+
+ this.activateTab(this.action);
+ }
+
+ /**
+ * Handles the `shown.bs.tab` event to set the currect url action.
+ *
+ * @param {type} evt
+ * @return {Function}
+ */
+ tabShown(evt) {
+ const source = evt.target.getAttribute('href');
+
+ return this.setCurrentAction(source);
+ }
+
+ /**
+ * Updates the URL with the path that matched the given action.
+ *
+ * @param {String} source
+ * @return {String}
+ */
+ setCurrentAction(source) {
+ const copySource = source;
+
+ copySource.replace(/\/+$/, '');
+
+ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
+
+ history.replaceState({
+ url: newState,
+ }, document.title, newState);
+ return newState;
+ }
+
+ /**
+ * Given the current action activates the correct tab.
+ * http://getbootstrap.com/javascript/#tab-show
+ * Note: Will trigger `shown.bs.tab`
+ */
+ activateTab() {
+ return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
+ }
+ };
+})();
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
deleted file mode 100644
index e810ee85bd3..00000000000
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * Linked Tabs
- *
- * Handles persisting and restores the current tab selection and content.
- * Reusable component for static content.
- *
- * ### Example Markup
- *
- * <ul class="nav-links tab-links">
- * <li class="active">
- * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1">
- * Tab 1
- * </a>
- * </li>
- * <li class="groups-tab">
- * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2">
- * Tab 2
- * </a>
- * </li>
- *
- *
- * <div class="tab-content">
- * <div class="tab-pane" id="tab1">
- * Tab 1 Content
- * </div>
- * <div class="tab-pane" id="tab2">
- * Tab 2 Content
- * </div>
- * </div>
- *
- *
- * ### How to use
- *
- * new window.gl.LinkedTabs({
- * action: "#{controller.action_name}",
- * defaultAction: 'tab1',
- * parentEl: '.tab-links'
- * });
- */
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.LinkedTabs = class LinkedTabs {
- /**
- * Binds the events and activates de default tab.
- *
- * @param {Object} options
- */
- constructor(options) {
- this.options = options || {};
-
- this.defaultAction = this.options.defaultAction;
- this.action = this.options.action || this.defaultAction;
-
- if (this.action === 'show') {
- this.action = this.defaultAction;
- }
-
- this.currentLocation = window.location;
-
- const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
-
- // since this is a custom event we need jQuery :(
- $(document)
- .off('shown.bs.tab', tabSelector)
- .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
-
- this.activateTab(this.action);
- }
-
- /**
- * Handles the `shown.bs.tab` event to set the currect url action.
- *
- * @param {type} evt
- * @return {Function}
- */
- tabShown(evt) {
- const source = evt.target.getAttribute('href');
-
- return this.setCurrentAction(source);
- }
-
- /**
- * Updates the URL with the path that matched the given action.
- *
- * @param {String} source
- * @return {String}
- */
- setCurrentAction(source) {
- const copySource = source;
-
- copySource.replace(/\/+$/, '');
-
- const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
-
- history.replaceState({
- turbolinks: true,
- url: newState,
- }, document.title, newState);
- return newState;
- }
-
- /**
- * Given the current action activates the correct tab.
- * http://getbootstrap.com/javascript/#tab-show
- * Note: Will trigger `shown.bs.tab`
- */
- activateTab() {
- return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
- }
- };
-})();
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 e3bff2559fd..00000000000
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ /dev/null
@@ -1,234 +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;
-
- var navbar = document.querySelector('.navbar-gitlab');
- var subnav = document.querySelector('.layout-nav');
- var fixedTabs = document.querySelector('.js-tabs-affix');
-
- var adjustment = 0;
- if (navbar) adjustment -= navbar.offsetHeight;
- if (subnav) adjustment -= subnav.offsetHeight;
-
- // 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);
- window.scrollBy(0, adjustment);
- }
- } else {
- // only adjust for fixedTabs when not targeting user-generated content
- if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
- }
- window.scrollBy(0, adjustment);
- }
- };
-
- // 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.scrollToElement = function($el) {
- var top = $el.offset().top;
- gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
- gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
- gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
-
- return $('body, html').animate({
- scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + 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;
- };
- })(window);
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 3ed8bfd5651..82dcbdc26c8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,12 +2,14 @@
/* global timeago */
/* global dateFormat */
-/*= require timeago */
-/*= require date.format */
+window.timeago = require('timeago.js');
+window.dateFormat = require('vendor/date.format');
(function() {
(function(w) {
var base;
+ var timeagoInstance;
+
if (w.gl == null) {
w.gl = {};
}
@@ -24,49 +26,51 @@
return this.days[date.getDay()];
};
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
- if (setTimeago == null) {
- setTimeago = true;
- }
-
- $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
- var $el = $(this);
- $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
+ $timeagoEls.each((i, el) => {
+ el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
if (setTimeago) {
// Recreate with custom template
- $el.tooltip({
+ $(el).tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
- $el.attr('data-timeago-rendered', true);
- gl.utils.renderTimeago($el);
+ el.classList.add('js-timeago-render');
});
+
+ gl.utils.renderTimeago($timeagoEls);
};
w.gl.utils.getTimeago = function() {
- var locale = function(number, index) {
- return [
- ['less than a minute ago', 'a while'],
- ['less than a minute ago', 'in %s seconds'],
- ['about a minute ago', 'in 1 minute'],
- ['%s minutes ago', 'in %s minutes'],
- ['about an hour ago', 'in 1 hour'],
- ['about %s hours ago', 'in %s hours'],
- ['a day ago', 'in 1 day'],
- ['%s days ago', 'in %s days'],
- ['a week ago', 'in 1 week'],
- ['%s weeks ago', 'in %s weeks'],
- ['a month ago', 'in 1 month'],
- ['%s months ago', 'in %s months'],
- ['a year ago', 'in 1 year'],
- ['%s years ago', 'in %s years']
- ][index];
- };
-
- timeago.register('gl_en', locale);
- return timeago();
+ var locale;
+
+ if (!timeagoInstance) {
+ locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
+ };
+
+ timeago.register('gl_en', locale);
+ timeagoInstance = timeago();
+ }
+
+ return timeagoInstance;
};
w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
@@ -85,9 +89,30 @@
return timefor;
};
- w.gl.utils.renderTimeago = function($element) {
- var timeagoInstance = gl.utils.getTimeago();
- timeagoInstance.render($element, 'gl_en');
+ w.gl.utils.cachedTimeagoElements = [];
+ w.gl.utils.renderTimeago = function($els) {
+ if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
+ w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
+ } else if ($els) {
+ w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
+ }
+
+ w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
+ };
+
+ w.gl.utils.updateTimeagoText = function(el) {
+ const timeago = gl.utils.getTimeago();
+ const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
+
+ if (el.textContent !== formattedDate) {
+ el.textContent = formattedDate;
+ }
+ };
+
+ w.gl.utils.initTimeagoTimeout = function() {
+ gl.utils.renderTimeago();
+
+ gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
};
w.gl.utils.getDayDifference = function(a, b) {
@@ -98,4 +123,4 @@
return Math.floor((date2 - date1) / millisecondsPerDay);
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
deleted file mode 100644
index aeb86c9fa5b..00000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-(function() {
- gl.emojiAliases = function() {
- return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
- };
-
-}).call(this);
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/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 6d5979603b9..66f39122a66 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -44,4 +44,4 @@
w.notify = notifyMe;
return w.notifyPermissions = notifyPermissions;
})(window);
-}).call(this);
+}).call(window);
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 6bb575059b7..2e5f8a09fc1 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,5 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
+require('vendor/latinise');
+
(function() {
(function(w) {
var base;
@@ -12,6 +14,9 @@
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
+ gl.text.highCountTrim = function(count) {
+ return count > 99 ? '99+' : count;
+ };
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
@@ -60,9 +65,10 @@
}
};
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) {
@@ -77,7 +83,17 @@
}
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) {
@@ -137,9 +153,8 @@
}
};
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);
@@ -161,8 +176,17 @@
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
- return gl.text.truncate = function(string, maxLength) {
+ gl.text.pluralize = function(str, count) {
+ return str + (count > 1 || count === 0 ? 's' : '');
+ };
+ gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
+ gl.text.dasherize = function(str) {
+ return str.replace(/[_\s]+/g, '-');
+ };
+ gl.text.slugify = function(str) {
+ return str.trim().toLowerCase().latinise();
+ };
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 6d813d61601..db62e0be324 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -12,4 +12,4 @@
return (obj != null) && (obj.constructor === Object);
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 8e15bf0735c..09c4261b318 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -66,6 +66,13 @@
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') {
@@ -76,5 +83,11 @@
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(this);
+}).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 eff1dcabfa2..00000000000
--- a/app/assets/javascripts/lib/vue_resource.js.es6
+++ /dev/null
@@ -1,2 +0,0 @@
-//= require vue
-//= require vue-resource
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 2f147704c22..1821ca18053 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -4,7 +4,7 @@
//
// Handles single- and multi-line selection and highlight for blob views.
//
-/*= require jquery.scrollTo */
+require('vendor/jquery.scrollTo');
//
// ### Example Markup
@@ -67,17 +67,7 @@
}
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) {
@@ -171,7 +161,6 @@
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
- turbolinks: false,
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
@@ -180,4 +169,4 @@
return LineHighlighter;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index ea9bfb4860a..729baa2e1a7 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,14 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
-/* global Turbolinks */
(function() {
- Turbolinks.enableProgressBar();
-
- $(document).on('page:fetch', function() {
+ window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate');
});
-
- $(document).on('page:change', function() {
- $('.tanuki-logo').removeClass('animate');
- });
-}).call(this);
+}).call(window);
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 b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..129d2dc5f0a
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,52 @@
+/* global Pikaday */
+/* global dateFormat */
+(() => {
+ // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+ // children of an element with the `clearable-input` class, and have a sibling
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ window.gl = window.gl || {};
+ gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+ const inputs = $(selector);
+
+ inputs.each((i, el) => {
+ const $input = $(el);
+
+ const calendar = new Pikaday({
+ field: $input.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ minDate: new Date(),
+ onSelect(dateText) {
+ $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+
+ $input.trigger('change');
+
+ toggleClearInput.call($input);
+ },
+ });
+
+ calendar.setDate(new Date($input.val()));
+ $input.data('pikaday', calendar);
+ });
+
+ inputs.next('.js-clear-input').on('click', function clicked(event) {
+ event.preventDefault();
+
+ const input = $(this).closest('.clearable-input').find(selector);
+ const calendar = input.data('pikaday');
+
+ calendar.setDate(null);
+ input.trigger('change');
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(window);
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6
deleted file mode 100644
index bf6c0ec2798..00000000000
--- a/app/assets/javascripts/member_expiration_date.js.es6
+++ /dev/null
@@ -1,36 +0,0 @@
-(() => {
- // Add datepickers to all `js-access-expiration-date` elements. If those elements are
- // children of an element with the `clearable-input` class, and have a sibling
- // `js-clear-input` element, then show that element when there is a value in the
- // datepicker, and make clicking on that element clear the field.
- //
- window.gl = window.gl || {};
- gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
- function toggleClearInput() {
- $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
- }
- const inputs = $(selector);
-
- inputs.datepicker({
- dateFormat: 'yy-mm-dd',
- minDate: 1,
- onSelect: function onSelect() {
- $(this).trigger('change');
- toggleClearInput.call(this);
- },
- });
-
- inputs.next('.js-clear-input').on('click', function clicked(event) {
- event.preventDefault();
-
- const input = $(this).closest('.clearable-input').find(selector);
- input.datepicker('setDate', null)
- .trigger('change');
- toggleClearInput.call(input);
- });
-
- inputs.on('blur', toggleClearInput);
-
- inputs.each(toggleClearInput);
- };
-}).call(this);
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 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
new file mode 100644
index 00000000000..653e52fb6bf
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -0,0 +1,92 @@
+/* eslint-disable new-cap, comma-dangle, no-new */
+/* global Vue */
+/* global Flash */
+
+window.Vue = require('vue');
+require('./merge_conflict_store');
+require('./merge_conflict_service');
+require('./mixins/line_conflict_utils');
+require('./mixins/line_conflict_actions');
+require('./components/diff_file_editor');
+require('./components/inline_conflict_lines');
+require('./components/parallel_conflict_lines');
+
+$(() => {
+ const INTERACTIVE_RESOLVE_MODE = 'interactive';
+ const conflictsEl = document.querySelector('#conflicts');
+ const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
+ const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
+ conflictsPath: conflictsEl.dataset.conflictsPath,
+ resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
+ });
+
+ gl.MergeConflictsResolverApp = new Vue({
+ el: '#conflicts',
+ data: mergeConflictsStore.state,
+ components: {
+ 'diff-file-editor': gl.mergeConflicts.diffFileEditor,
+ 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
+ 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
+ },
+ computed: {
+ conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
+ readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
+ commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
+ showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
+ },
+ created() {
+ mergeConflictsService
+ .fetchConflictsData()
+ .done((data) => {
+ if (data.type === 'error') {
+ mergeConflictsStore.setFailedRequest(data.message);
+ } else {
+ mergeConflictsStore.setConflictsData(data);
+ }
+ })
+ .error(() => {
+ mergeConflictsStore.setFailedRequest();
+ })
+ .always(() => {
+ mergeConflictsStore.setLoadingState(false);
+
+ this.$nextTick(() => {
+ $('.js-syntax-highlight').syntaxHighlight();
+ });
+ });
+ },
+ methods: {
+ handleViewTypeChange(viewType) {
+ mergeConflictsStore.setViewType(viewType);
+ },
+ onClickResolveModeButton(file, mode) {
+ if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
+ mergeConflictsStore.setPromptConfirmationState(file, true);
+ return;
+ }
+
+ mergeConflictsStore.setFileResolveMode(file, mode);
+ },
+ acceptDiscardConfirmation(file) {
+ mergeConflictsStore.setPromptConfirmationState(file, false);
+ mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
+ },
+ cancelDiscardConfirmation(file) {
+ mergeConflictsStore.setPromptConfirmationState(file, false);
+ },
+ commit() {
+ mergeConflictsStore.setSubmitState(true);
+
+ mergeConflictsService
+ .submitResolveConflicts(mergeConflictsStore.getCommitData())
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ mergeConflictsStore.setSubmitState(false);
+ new Flash('Failed to save merge conflicts resolutions. Please try again!');
+ });
+ }
+ }
+ });
+});
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
deleted file mode 100644
index a2d90f9ba47..00000000000
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable new-cap, comma-dangle, no-new */
-/* global Vue */
-/* global Flash */
-
-//= require vue
-//= require ./merge_conflict_store
-//= require ./merge_conflict_service
-//= require ./mixins/line_conflict_utils
-//= require ./mixins/line_conflict_actions
-//= require ./components/diff_file_editor
-//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_lines
-
-$(() => {
- const INTERACTIVE_RESOLVE_MODE = 'interactive';
- const conflictsEl = document.querySelector('#conflicts');
- const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
- const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
- conflictsPath: conflictsEl.dataset.conflictsPath,
- resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
- });
-
- gl.MergeConflictsResolverApp = new Vue({
- el: '#conflicts',
- data: mergeConflictsStore.state,
- components: {
- 'diff-file-editor': gl.mergeConflicts.diffFileEditor,
- 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
- 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
- },
- computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
- },
- created() {
- mergeConflictsService
- .fetchConflictsData()
- .done((data) => {
- if (data.type === 'error') {
- mergeConflictsStore.setFailedRequest(data.message);
- } else {
- mergeConflictsStore.setConflictsData(data);
- }
- })
- .error(() => {
- mergeConflictsStore.setFailedRequest();
- })
- .always(() => {
- mergeConflictsStore.setLoadingState(false);
-
- this.$nextTick(() => {
- $('.js-syntax-highlight').syntaxHighlight();
- });
- });
- },
- methods: {
- handleViewTypeChange(viewType) {
- mergeConflictsStore.setViewType(viewType);
- },
- onClickResolveModeButton(file, mode) {
- if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
- mergeConflictsStore.setPromptConfirmationState(file, true);
- return;
- }
-
- mergeConflictsStore.setFileResolveMode(file, mode);
- },
- acceptDiscardConfirmation(file) {
- mergeConflictsStore.setPromptConfirmationState(file, false);
- mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
- },
- cancelDiscardConfirmation(file) {
- mergeConflictsStore.setPromptConfirmationState(file, false);
- },
- commit() {
- mergeConflictsStore.setSubmitState(true);
-
- mergeConflictsService
- .submitResolveConflicts(mergeConflictsStore.getCommitData())
- .done((data) => {
- window.location.href = data.redirect_to;
- })
- .error(() => {
- mergeConflictsStore.setSubmitState(false);
- new Flash('Failed to save merge conflicts resolutions. Please try again!');
- });
- }
- }
- });
-});
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.js b/app/assets/javascripts/merge_request.js
index 09ee8dbe9d7..5e01aacf2ba 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
/* global MergeRequestTabs */
-/*= require jquery.waitforimages */
-/*= require task_list */
-/*= require merge_request_tabs */
+require('vendor/jquery.waitforimages');
+require('./task_list');
+require('./merge_request_tabs');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -24,12 +24,18 @@
};
})(this));
this.initTabs();
- // Prevent duplicate event bindings
- this.disableTaskList();
this.initMRBtnListeners();
this.initCommitMessageListeners();
if ($("a.btn-close").length) {
- this.initTaskList();
+ this.taskList = new gl.TaskList({
+ dataType: 'merge_request',
+ 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;
+ }
+ });
}
}
@@ -50,11 +56,6 @@
return this.$('.all-commits').removeClass('hide');
};
- MergeRequest.prototype.initTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('enable');
- return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
- };
-
MergeRequest.prototype.initMRBtnListeners = function() {
var _this;
_this = this;
@@ -85,50 +86,26 @@
}
};
- MergeRequest.prototype.disableTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
- };
-
- MergeRequest.prototype.updateTaskList = function() {
- var patchData;
- patchData = {};
- patchData['merge_request'] = {
- 'description': $('.js-task-list-field', this).val()
- };
- return $.ajax({
- type: 'PATCH',
- url: $('form.js-issuable-update').attr('action'),
- data: patchData,
- success: function(mergeRequest) {
- document.querySelector('#task_status').innerText = mergeRequest.task_status;
- document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
- }
- });
- // TODO (rspeicher): Make the merge request description inline-editable like a
- // note so that we can re-use its form here
- };
-
MergeRequest.prototype.initCommitMessageListeners = function() {
- var textarea = $('textarea.js-commit-message');
-
- $('a.js-with-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-with-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
- $('p.js-with-description-hint').hide();
- $('p.js-without-description-hint').show();
+ $('.js-with-description-hint').hide();
+ $('.js-without-description-hint').show();
});
- $('a.js-without-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-without-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
- $('p.js-with-description-hint').show();
- $('p.js-without-description-hint').hide();
+ $('.js-with-description-hint').show();
+ $('.js-without-description-hint').hide();
});
};
return MergeRequest;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
new file mode 100644
index 00000000000..190336dbd20
--- /dev/null
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -0,0 +1,358 @@
+/* eslint-disable no-new, class-methods-use-this */
+/* global Breakpoints */
+/* global Cookies */
+/* global Flash */
+
+require('./breakpoints');
+window.Cookies = require('js-cookie');
+require('./flash');
+
+/* eslint-disable max-len */
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
+// ### Example Markup
+//
+// <ul class="nav-links merge-request-tabs">
+// <li class="notes-tab active">
+// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+// Discussion
+// </a>
+// </li>
+// <li class="commits-tab">
+// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+// Commits
+// </a>
+// </li>
+// <li class="diffs-tab">
+// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+// Diffs
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="notes tab-pane active" id="notes">
+// Notes Content
+// </div>
+// <div class="commits tab-pane" id="commits">
+// Commits Content
+// </div>
+// <div class="diffs tab-pane" id="diffs">
+// Diffs Content
+// </div>
+// </div>
+//
+// <div class="mr-loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
+/* eslint-enable max-len */
+
+(() => {
+ // Store the `location` object, allowing for easier stubbing in tests
+ let location = window.location;
+
+ class MergeRequestTabs {
+
+ constructor({ action, setUrl, stubLocation } = {}) {
+ this.diffsLoaded = false;
+ this.pipelinesLoaded = false;
+ this.commitsLoaded = false;
+ this.fixedLayoutPref = null;
+
+ this.setUrl = setUrl !== undefined ? setUrl : true;
+ this.setCurrentAction = this.setCurrentAction.bind(this);
+ this.tabShown = this.tabShown.bind(this);
+ this.showTab = this.showTab.bind(this);
+
+ if (stubLocation) {
+ location = stubLocation;
+ }
+
+ this.bindEvents();
+ this.activateTab(action);
+ this.initAffix();
+ }
+
+ bindEvents() {
+ $(document)
+ .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .on('click', this.clickTab);
+ }
+
+ unbindEvents() {
+ $(document)
+ .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .off('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
+ }
+
+ showTab(e) {
+ e.preventDefault();
+ this.activateTab($(e.target).data('action'));
+ }
+
+ clickTab(e) {
+ if (e.currentTarget && gl.utils.isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ window.open(targetLink, '_blank');
+ }
+ }
+
+ tabShown(e) {
+ const $target = $(e.target);
+ const action = $target.data('action');
+
+ if (action === 'commits') {
+ this.loadCommits($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else if (this.isDiffAction(action)) {
+ this.loadDiff($target.attr('href'));
+ if (Breakpoints.get().getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ $.scrollTo('.merge-request-details .merge-request-tabs', {
+ offset: 0,
+ });
+ } else if (action === 'pipelines') {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
+ this.pipelinesLoaded = true;
+ } else {
+ this.expandView();
+ this.resetViewContainer();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
+ }
+ }
+
+ scrollToElement(container) {
+ if (location.hash) {
+ const offset = -$('.js-tabs-affix').outerHeight();
+ const $el = $(`${container} ${location.hash}:not(.match)`);
+ if ($el.length) {
+ $.scrollTo($el[0], { offset });
+ }
+ }
+ }
+
+ // Activate a tab based on the current action
+ activateTab(action) {
+ const activate = action === 'show' ? 'notes' : action;
+ // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
+ $(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
+ }
+
+ // Replaces the current Merge Request-specific action in the URL with a new one
+ //
+ // If the action is "notes", the URL is reset to the standard
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('notes')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
+ setCurrentAction(action) {
+ this.currentAction = action === 'show' ? 'notes' : action;
+
+ // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
+ // Append the new action if we're on a tab other than 'notes'
+ if (this.currentAction !== 'notes') {
+ newState += `/${this.currentAction}`;
+ }
+
+ // Ensure parameters and hash come along for the ride
+ newState += location.search + location.hash;
+
+ // TODO: Consider refactoring in light of turbolinks removal.
+
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
+ window.history.replaceState({
+ url: newState,
+ }, document.title, newState);
+
+ return newState;
+ }
+
+ loadCommits(source) {
+ if (this.commitsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#commits').innerHTML = data.html;
+ gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
+ this.commitsLoaded = true;
+ this.scrollToElement('#commits');
+ },
+ });
+ }
+
+ loadDiff(source) {
+ if (this.diffsLoaded) {
+ return;
+ }
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const urlPathname = gl.utils.parseUrlPathname(source);
+
+ this.ajaxGet({
+ url: `${urlPathname}.json${location.search}`,
+ success: (data) => {
+ $('#diffs').html(data.html);
+
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+
+ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
+ $('#diffs .js-syntax-highlight').syntaxHighlight();
+
+ if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ this.expandViewContainer();
+ }
+ this.diffsLoaded = true;
+
+ new gl.Diff();
+ this.scrollToElement('#diffs');
+ },
+ });
+ }
+
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
+ toggleLoading(status) {
+ $('.mr-loading-status .loading').toggle(status);
+ }
+
+ ajaxGet(options) {
+ const defaults = {
+ beforeSend: () => this.toggleLoading(true),
+ error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ };
+ $.ajax($.extend({}, defaults, options));
+ }
+
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ isDiffAction(action) {
+ return action === 'diffs' || action === 'new/diffs';
+ }
+
+ expandViewContainer() {
+ const $wrapper = $('.content-wrapper .container-fluid');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+ }
+ $wrapper.removeClass('container-limited');
+ }
+
+ resetViewContainer() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
+ }
+ }
+
+ shrinkView() {
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is expanded
+ if ($gutterIcon.is('.fa-angle-double-right')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView() {
+ if (Cookies.get('collapsed_gutter') === 'true') {
+ return;
+ }
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is collapsed
+ if ($gutterIcon.is('.fa-angle-double-left')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ initAffix() {
+ const $tabs = $('.js-tabs-affix');
+
+ // Screen space on small screens is usually very sparse
+ // So we dont affix the tabs on these
+ if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+ const $diffTabs = $('#diff-notes-app');
+
+ $tabs.off('affix.bs.affix affix-top.bs.affix')
+ .affix({
+ offset: {
+ top: () => (
+ $diffTabs.offset().top - $tabs.height()
+ ),
+ },
+ })
+ .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
+ .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
+
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MergeRequestTabs = MergeRequestTabs;
+})();
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
deleted file mode 100644
index 4c8c28af755..00000000000
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ /dev/null
@@ -1,365 +0,0 @@
-/* eslint-disable no-new, class-methods-use-this */
-/* global Breakpoints */
-/* global Cookies */
-/* global DiffNotesApp */
-/* global Flash */
-
-/*= require js.cookie */
-/*= require breakpoints */
-
-/* eslint-disable max-len */
-// MergeRequestTabs
-//
-// Handles persisting and restoring the current tab selection and lazily-loading
-// content on the MergeRequests#show page.
-//
-// ### Example Markup
-//
-// <ul class="nav-links merge-request-tabs">
-// <li class="notes-tab active">
-// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
-// Discussion
-// </a>
-// </li>
-// <li class="commits-tab">
-// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
-// Commits
-// </a>
-// </li>
-// <li class="diffs-tab">
-// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
-// Diffs
-// </a>
-// </li>
-// </ul>
-//
-// <div class="tab-content">
-// <div class="notes tab-pane active" id="notes">
-// Notes Content
-// </div>
-// <div class="commits tab-pane" id="commits">
-// Commits Content
-// </div>
-// <div class="diffs tab-pane" id="diffs">
-// Diffs Content
-// </div>
-// </div>
-//
-// <div class="mr-loading-status">
-// <div class="loading">
-// Loading Animation
-// </div>
-// </div>
-//
-/* eslint-enable max-len */
-
-(() => {
- // Store the `location` object, allowing for easier stubbing in tests
- let location = window.location;
-
- class MergeRequestTabs {
-
- constructor({ action, setUrl, stubLocation } = {}) {
- this.diffsLoaded = false;
- this.pipelinesLoaded = false;
- this.commitsLoaded = false;
- this.fixedLayoutPref = null;
-
- this.setUrl = setUrl !== undefined ? setUrl : true;
- this.setCurrentAction = this.setCurrentAction.bind(this);
- this.tabShown = this.tabShown.bind(this);
- this.showTab = this.showTab.bind(this);
-
- if (stubLocation) {
- location = stubLocation;
- }
-
- this.bindEvents();
- this.activateTab(action);
- this.initAffix();
- }
-
- bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
- }
-
- unbindEvents() {
- $(document)
- .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .off('click', '.js-show-tab', this.showTab);
- }
-
- showTab(e) {
- e.preventDefault();
- this.activateTab($(e.target).data('action'));
- }
-
- tabShown(e) {
- const $target = $(e.target);
- const action = $target.data('action');
-
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
- if (Breakpoints.get().getBreakpointSize() !== 'lg') {
- this.shrinkView();
- }
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
- const navBarHeight = $('.navbar-gitlab').outerHeight();
- $.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: -navBarHeight,
- });
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else {
- this.expandView();
- this.resetViewContainer();
- }
- if (this.setUrl) {
- this.setCurrentAction(action);
- }
- }
-
- scrollToElement(container) {
- if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.layout-nav').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
- const $el = $(`${container} ${location.hash}:not(.match)`);
- if ($el.length) {
- $.scrollTo($el[0], { offset });
- }
- }
- }
-
- // Activate a tab based on the current action
- activateTab(action) {
- const activate = action === 'show' ? 'notes' : action;
- // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
- $(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
- }
-
- // Replaces the current Merge Request-specific action in the URL with a new one
- //
- // If the action is "notes", the URL is reset to the standard
- // `MergeRequests#show` route.
- //
- // Examples:
- //
- // location.pathname # => "/namespace/project/merge_requests/1"
- // setCurrentAction('diffs')
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('notes')
- // location.pathname # => "/namespace/project/merge_requests/1"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('commits')
- // location.pathname # => "/namespace/project/merge_requests/1/commits"
- //
- // Returns the new URL String
- setCurrentAction(action) {
- this.currentAction = action === 'show' ? 'notes' : action;
-
- // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
- let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
-
- // Append the new action if we're on a tab other than 'notes'
- if (this.currentAction !== 'notes') {
- newState += `/${this.currentAction}`;
- }
-
- // Ensure parameters and hash come along for the ride
- newState += location.search + location.hash;
-
- // Replace the current history state with the new one without breaking
- // Turbolinks' history.
- //
- // See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- turbolinks: true,
- url: newState,
- }, document.title, newState);
-
- return newState;
- }
-
- loadCommits(source) {
- if (this.commitsLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- document.querySelector('div#commits').innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
- this.commitsLoaded = true;
- this.scrollToElement('#commits');
- },
- });
- }
-
- loadDiff(source) {
- if (this.diffsLoaded) {
- return;
- }
-
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- const urlPathname = gl.utils.parseUrlPathname(source);
-
- this.ajaxGet({
- url: `${urlPathname}.json${location.search}`,
- success: (data) => {
- $('#diffs').html(data.html);
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
-
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
- $('#diffs .js-syntax-highlight').syntaxHighlight();
-
- if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
- this.expandViewContainer();
- }
- this.diffsLoaded = true;
-
- new gl.Diff();
- this.scrollToElement('#diffs');
- },
- });
- }
-
- loadPipelines(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- this.scrollToElement('#pipelines');
-
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- },
- });
- }
-
- // Show or hide the loading spinner
- //
- // status - Boolean, true to show, false to hide
- toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
- }
-
- ajaxGet(options) {
- const defaults = {
- beforeSend: () => this.toggleLoading(true),
- error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- type: 'GET',
- };
- $.ajax($.extend({}, defaults, options));
- }
-
- diffViewType() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
-
- isDiffAction(action) {
- return action === 'diffs' || action === 'new/diffs';
- }
-
- expandViewContainer() {
- const $wrapper = $('.content-wrapper .container-fluid');
- if (this.fixedLayoutPref === null) {
- this.fixedLayoutPref = $wrapper.hasClass('container-limited');
- }
- $wrapper.removeClass('container-limited');
- }
-
- resetViewContainer() {
- if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
- }
- }
-
- shrinkView() {
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
-
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is expanded
- if ($gutterIcon.is('.fa-angle-double-right')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- }
-
- // Expand the issuable sidebar unless the user explicitly collapsed it
- expandView() {
- if (Cookies.get('collapsed_gutter') === 'true') {
- return;
- }
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
-
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is collapsed
- if ($gutterIcon.is('.fa-angle-double-left')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- }
-
- initAffix() {
- const $tabs = $('.js-tabs-affix');
-
- // Screen space on small screens is usually very sparse
- // So we dont affix the tabs on these
- if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- const $diffTabs = $('#diff-notes-app');
- const $fixedNav = $('.navbar-fixed-top');
- const $layoutNav = $('.layout-nav');
-
- $tabs.off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height()
- ),
- },
- })
- .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
- .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
- }
- }
-
- window.gl = window.gl || {};
- window.gl.MergeRequestTabs = MergeRequestTabs;
-})();
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
new file mode 100644
index 00000000000..66cc270ab4d
--- /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" 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) === -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) {
+ 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 fa782ebbedf..00000000000
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ /dev/null
@@ -1,273 +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 */
-/* global Turbolinks */
-
-((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.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('page:change.merge_request');
- };
-
- MergeRequestWidget.prototype.addEventListeners = function() {
- var allowedPages;
- allowedPages = ['show', 'commits', 'pipelines', 'changes'];
- $(document).on('page:change.merge_request', (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 _this.$widgetBody.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":
- return this.setMergeButtonClass('btn-danger');
- case "running":
- return this.setMergeButtonClass('btn-info');
- case "success":
- case "success_with_warnings":
- return this.setMergeButtonClass('btn-create');
- }
- } else {
- $('.ci_widget.ci-error').show();
- return 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('/'));
- };
-
- 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 5969d2ba56b..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 {
- merge_request_widget.getMergeStatus();
- }
- });
-})();
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 527cdc9b698..9548a98f499 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -42,4 +42,4 @@
return MergedButtons;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7ce1259e015..38c673e8907 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
+/* global Sortable */
(function() {
this.Milestone = (function() {
@@ -8,11 +9,9 @@
type: "PUT",
url: issue_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -27,11 +26,9 @@
type: "PUT",
url: sort_issues_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function() {
return new Flash("Issues update failed", 'alert');
},
@@ -46,11 +43,9 @@
type: "PUT",
url: sort_mr_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -63,11 +58,9 @@
type: "PUT",
url: merge_request_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -81,65 +74,29 @@
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16');
- $(element).find('.assignee-icon').html(img_tag);
+ $(element).find('.assignee-icon img').replaceWith(img_tag);
} else {
- $(element).find('.assignee-icon').html('');
+ $(element).find('.assignee-icon').empty();
}
- return $(element).effect('highlight');
};
function Milestone() {
var oldMouseStart;
- oldMouseStart = $.ui.sortable.prototype._mouseStart;
- $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
- this._trigger("beforeStart", event, this._uiHash());
- return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
- };
this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching();
}
Milestone.prototype.bindIssuesSorting = function() {
- return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
- connectWith: ".issues-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
- },
- stop: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", "0px");
- },
- update: function(event, ui) {
- var data;
- // Prevents sorting from container which element has been removed.
- if ($(this).find(ui.item).length > 0) {
- data = $(this).sortable("serialize");
- return Milestone.sortIssues(data);
- }
- },
- receive: function(event, ui) {
- var data, issue_id, issue_url, new_state;
- new_state = $(this).data('state');
- issue_id = ui.item.data('iid');
- issue_url = ui.item.data('url');
- data = (function() {
- switch (new_state) {
- case 'ongoing':
- return "issue[assignee_id]=" + gon.current_user_id;
- case 'unassigned':
- return "issue[assignee_id]=";
- case 'closed':
- return "issue[state_event]=close";
- }
- })();
- if ($(ui.sender).data('state') === "closed") {
- data += "&issue[state_event]=reopen";
- }
- return Milestone.updateIssue(ui.item, issue_url, data);
- }
- }).disableSelection();
+ $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
+ this.createSortable(el, {
+ group: 'issue-list',
+ listEls: $('.issues-sortable-list'),
+ fieldName: 'issue',
+ sortCallback: Milestone.sortIssues,
+ updateCallback: Milestone.updateIssue,
+ });
+ }.bind(this));
};
Milestone.prototype.bindTabsSwitching = function() {
@@ -154,44 +111,64 @@
};
Milestone.prototype.bindMergeRequestSorting = function() {
- return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({
- connectWith: ".merge_requests-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
+ $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
+ this.createSortable(el, {
+ group: 'merge-request-list',
+ listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
+ fieldName: 'merge_request',
+ sortCallback: Milestone.sortMergeRequests,
+ updateCallback: Milestone.updateMergeRequest,
+ });
+ }.bind(this));
+ };
+
+ Milestone.prototype.createSortable = function(el, opts) {
+ return Sortable.create(el, {
+ group: opts.group,
+ filter: '.is-disabled',
+ forceFallback: true,
+ onStart: function(e) {
+ opts.listEls.css('min-height', e.item.offsetHeight);
},
- stop: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", "0px");
+ onEnd: function () {
+ opts.listEls.css("min-height", "0px");
},
- update: function(event, ui) {
- var data;
- data = $(this).sortable("serialize");
- return Milestone.sortMergeRequests(data);
+ onUpdate: function(e) {
+ var ids = this.toArray(),
+ data;
+
+ if (ids.length) {
+ data = ids.map(function(id) {
+ return 'sortable_' + opts.fieldName + '[]=' + id;
+ }).join('&');
+
+ opts.sortCallback(data);
+ }
},
- receive: function(event, ui) {
- var data, merge_request_id, merge_request_url, new_state;
- new_state = $(this).data('state');
- merge_request_id = ui.item.data('iid');
- merge_request_url = ui.item.data('url');
+ onAdd: function (e) {
+ var data, issuableId, issuableUrl, newState;
+ newState = e.to.dataset.state;
+ issuableUrl = e.item.dataset.url;
data = (function() {
- switch (new_state) {
+ switch (newState) {
case 'ongoing':
- return "merge_request[assignee_id]=" + gon.current_user_id;
+ return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
case 'unassigned':
- return "merge_request[assignee_id]=";
+ return opts.fieldName + '[assignee_id]=';
case 'closed':
- return "merge_request[state_event]=close";
+ return opts.fieldName + '[state_event]=close';
}
})();
- if ($(ui.sender).data('state') === "closed") {
- data += "&merge_request[state_event]=reopen";
+ if (e.from.dataset.state === 'closed') {
+ data += '&' + opts.fieldName + '[state_event]=reopen';
}
- return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
+
+ opts.updateCallback(e.item, issuableUrl, data);
+ this.options.onUpdate.call(this, e);
}
- }).disableSelection();
+ });
};
return Milestone;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 7ab39ffbd05..51fa5c828b3 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -5,13 +5,20 @@
(function() {
this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject) {
- var _this;
+ function MilestoneSelect(currentProject, els) {
+ var _this, $els;
if (currentProject != null) {
_this = this;
this.currentProject = JSON.parse(currentProject);
}
- $('.js-milestone-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-milestone-select');
+ }
+
+ $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;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
@@ -32,7 +39,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>');
}
@@ -108,7 +115,7 @@
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
- var data, isIssueIndex, isMRIndex, page;
+ var data, isIssueIndex, isMRIndex, page, boardsStore;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
@@ -116,9 +123,19 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
- gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+
+ 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) {
+ 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) {
@@ -164,8 +181,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));
@@ -182,4 +198,4 @@
return MilestoneSelect;
})();
-}).call(this);
+}).call(window);
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 80549532ea9..00000000000
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ /dev/null
@@ -1,97 +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);
-
- this.bindEvents();
- }
-
- /**
- * Adds the event listener when the dropdown is opened.
- * All dropdown events are fired at the .dropdown-menu's parent element.
- */
- bindEvents() {
- $(this.container).on('shown.bs.dropdown', 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/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
new file mode 100644
index 00000000000..71eb746edac
--- /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 _ from 'underscore';
+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() {
+ const self = this;
+ _.each(this.data, (value, key) => {
+ if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
+ self.plotValues(value, key);
+ }
+ });
+ }
+
+ init() {
+ const self = this;
+ this.getData().then((metricsResponse) => {
+ if (metricsResponse === {}) {
+ new Flash('Empty metrics', 'alert');
+ } else {
+ self.transformData(metricsResponse);
+ self.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 = {};
+ _.each(metricsResponse.metrics, (value, key) => {
+ const metricValues = value[0].values;
+ metricTypes[key] = _.map(metricValues, metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ });
+ this.data = metricTypes;
+ }
+}
+
+export default PrometheusGraph;
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 514556ade0b..b98e6121967 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -29,7 +29,7 @@
if (selected.id == null) {
return selected.text;
} else {
- return selected.kind + ": " + selected.path;
+ return selected.kind + ": " + selected.full_path;
}
},
data: function(term, dataCallback) {
@@ -50,7 +50,7 @@
if (namespace.id == null) {
return namespace.text;
} else {
- return namespace.kind + ": " + namespace.path;
+ return namespace.kind + ": " + namespace.full_path;
}
},
renderRow: this.renderRow,
@@ -83,4 +83,4 @@
return NamespaceSelects;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index a7ccd03b60c..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(this);
+
+ return BranchGraph;
+})();
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 37bf6436fd1..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(this);
+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 2e6eb83cec7..8aae2ad201c 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,26 +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 */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
+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(this);
+ 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 7f763c13b50..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;
@@ -100,4 +120,4 @@
return NewBranchForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 41eea78a3e6..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) {
@@ -30,4 +34,4 @@
return NewCommitForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c4722be3625..47cc34e7a20 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,16 +1,19 @@
/* 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 */
-/*= require autosize */
-/*= require dropzone */
-/*= require dropzone_input */
-/*= require gfm_auto_complete */
-/*= require jquery.atwho */
-/*= require task_list */
+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
+require('vendor/jquery.atwho');
+require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -41,7 +44,6 @@
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;
@@ -50,8 +52,13 @@
this.addBinding();
this.setPollingInterval();
this.setupMainTargetNoteForm();
- this.initTaskList();
+ this.taskList = new gl.TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ 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') {
@@ -60,6 +67,10 @@
}
}
+ 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);
@@ -124,8 +135,6 @@
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
$(document).off("click", '.system-note-commit-list-toggler');
- $('.note .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
Notes.prototype.keydownNoteText = function(e) {
@@ -195,7 +204,7 @@
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) {
@@ -243,12 +252,21 @@
};
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();
+ }
}
};
@@ -259,33 +277,23 @@
*/
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();
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList();
- this.initTaskList();
+ this.taskList.init();
this.refresh();
return this.updateNotesCount(1);
}
@@ -300,7 +308,7 @@
};
Notes.prototype.isParallelView = function() {
- return this.view === 'parallel';
+ return Cookies.get('diff_view') === 'parallel';
};
/*
@@ -310,7 +318,7 @@
*/
Notes.prototype.renderDiscussionNote = function(note) {
- var discussionContainer, form, note_html, row;
+ var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) {
return;
}
@@ -320,6 +328,8 @@
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?
@@ -328,10 +338,26 @@
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
@@ -345,14 +371,40 @@
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.
@@ -454,7 +506,7 @@
var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
}
}
@@ -590,9 +642,14 @@
*/
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.
@@ -602,17 +659,26 @@
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();
};
@@ -705,15 +771,16 @@
*/
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");
@@ -721,7 +788,9 @@
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) {
@@ -738,13 +807,21 @@
}
}
}
- } 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);
@@ -862,15 +939,6 @@
}
};
- Notes.prototype.initTaskList = function() {
- this.enableTaskList();
- return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this));
- };
-
- Notes.prototype.enableTaskList = function() {
- return $('.note .js-task-list-container').taskList('enable');
- };
-
Notes.prototype.putEditFormInPlace = function($el) {
var $editForm = $(this.getEditFormSelector($el));
var $note = $el.closest('.note');
@@ -895,17 +963,6 @@
$editForm.find('.referenced-users').hide();
};
- Notes.prototype.updateTaskList = function(e) {
- var $target = $(e.target);
- var $list = $target.closest('.js-task-list-container');
- var $editForm = $(this.getEditFormSelector($target));
- var $note = $list.closest('.note');
-
- this.putEditFormInPlace($list);
- $editForm.find('#note_note').val($note.find('.original-task-list').val());
- $('form', $list).submit();
- };
-
Notes.prototype.updateNotesCount = function(updateCount) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
@@ -922,9 +979,10 @@
};
Notes.prototype.toggleCommitList = function(e) {
- const $element = $(e.target);
+ const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
+ $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
};
@@ -953,4 +1011,4 @@
return Notes;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 926dc35fee8..838356133cd 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -28,4 +28,4 @@
return NotificationsDropdown;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index c3d7cc0adfb..5005af90d48 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -54,4 +54,4 @@
return NotificationsForm;
})();
-}).call(this);
+}).call(window);
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 b/app/assets/javascripts/pipelines.js
new file mode 100644
index 00000000000..9203abefbbc
--- /dev/null
+++ b/app/assets/javascripts/pipelines.js
@@ -0,0 +1,38 @@
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
+
+require('./lib/utils/bootstrap_linked_tabs');
+
+((global) => {
+ class Pipelines {
+ constructor(options = {}) {
+ if (options.initTabs && options.tabsOptions) {
+ new global.LinkedTabs(options.tabsOptions);
+ }
+
+ this.addMarginToBuildColumns();
+ }
+
+ addMarginToBuildColumns() {
+ this.pipelineGraph = document.querySelector('.js-pipeline-graph');
+
+ const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+
+ for (const buildNodeIndex in secondChildBuildNodes) {
+ const buildNode = secondChildBuildNodes[buildNodeIndex];
+ const firstChildBuildNode = buildNode.previousElementSibling;
+ if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
+ const multiBuildColumn = buildNode.closest('.stage-column');
+ const previousColumn = multiBuildColumn.previousElementSibling;
+ if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
+ multiBuildColumn.classList.add('left-margin');
+ firstChildBuildNode.classList.add('left-connector');
+ const columnBuilds = previousColumn.querySelectorAll('.build');
+ if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
+ }
+
+ this.pipelineGraph.classList.remove('hidden');
+ }
+ }
+
+ global.Pipelines = Pipelines;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
deleted file mode 100644
index 43263368494..00000000000
--- a/app/assets/javascripts/pipelines.js.es6
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
-
-//= require lib/utils/bootstrap_linked_tabs
-
-((global) => {
- class Pipelines {
- constructor(options = {}) {
- if (options.initTabs && options.tabsOptions) {
- new global.LinkedTabs(options.tabsOptions);
- }
-
- this.addMarginToBuildColumns();
- }
-
- addMarginToBuildColumns() {
- this.pipelineGraph = document.querySelector('.js-pipeline-graph');
-
- const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
-
- for (const buildNodeIndex in secondChildBuildNodes) {
- const buildNode = secondChildBuildNodes[buildNodeIndex];
- const firstChildBuildNode = buildNode.previousElementSibling;
- if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
- const multiBuildColumn = buildNode.closest('.stage-column');
- const previousColumn = multiBuildColumn.previousElementSibling;
- if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
- multiBuildColumn.classList.add('left-margin');
- firstChildBuildNode.classList.add('left-connector');
- const columnBuilds = previousColumn.querySelectorAll('.build');
- if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
- }
-
- this.pipelineGraph.classList.remove('hidden');
- }
- }
-
- global.Pipelines = Pipelines;
-})(window.gl || (window.gl = {}));
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..4ccea0624ee
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js
@@ -0,0 +1,100 @@
+/* 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('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 5aec9c813fe..00000000000
--- a/app/assets/javascripts/profile/profile.js.es6
+++ /dev/null
@@ -1,98 +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);
- $('.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 f50802bdf2e..15d32825583 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,7 +1,2 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+require('./gl_crop');
+require('./profile');
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 7cf630a1d76..db7ceaa2421 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global Cookies */
-/* global Turbolinks */
/* global ProjectSelect */
(function() {
@@ -58,6 +57,11 @@
};
Project.prototype.initRefSwitcher = function() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
return $('.js-project-refs-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -67,7 +71,8 @@
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
- ref: $dropdown.data('ref')
+ ref: $dropdown.data('ref'),
+ search: term
},
dataType: "json"
}).done(function(refs) {
@@ -76,16 +81,29 @@
},
selectable: true,
filterable: true,
+ filterRemote: true,
filterByText: true,
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
- var link;
+ var li = refListItem.cloneNode(false);
+
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
- return $('<li />').append(link);
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
+ }
+
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
}
+
+ return li;
},
id: function(obj, $el) {
return $el.attr('data-ref');
@@ -98,8 +116,8 @@
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
var action = $form.attr('action');
- var divider = action.indexOf('?') < 0 ? '?' : '&';
- Turbolinks.visit(action + '' + divider + '' + $form.serialize());
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
+ gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
});
@@ -108,4 +126,4 @@
return Project;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index a6d3ba9eb86..aabdfbf65e2 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -17,4 +17,4 @@
return ProjectAvatar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 04fe84683f3..e01668eabef 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -168,4 +168,4 @@
return ProjectFindFile;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 208f25a0e33..47197db39d3 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -10,4 +10,4 @@
return ProjectFork;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 6614d8952cd..08334bf1ec5 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,14 +1,13 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
-/* global Turbolinks */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.visitUrl(location.href);
}, 5000);
}
return ProjectImport;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
new file mode 100644
index 00000000000..0a811627600
--- /dev/null
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -0,0 +1,55 @@
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
+
+(function(global) {
+ class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
+
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
+
+ toggleSubscription(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
+
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ let newStatus, newAction;
+
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
+
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
+
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
+
+ this.$buttons.map((button) => {
+ const $button = $(button);
+
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
+
+ return button;
+ });
+ });
+ }
+ }
+
+ global.ProjectLabelSubscription = ProjectLabelSubscription;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
deleted file mode 100644
index 8365f7118d5..00000000000
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
-
-(function(global) {
- class ProjectLabelSubscription {
- constructor(container) {
- this.$container = $(container);
- this.$buttons = this.$container.find('.js-subscribe-button');
-
- this.$buttons.on('click', this.toggleSubscription.bind(this));
- }
-
- toggleSubscription(event) {
- event.preventDefault();
-
- const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
- const url = $btn.attr('data-url');
- const oldStatus = $btn.attr('data-status');
-
- $btn.addClass('disabled');
- $span.toggleClass('hidden');
-
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- let newStatus, newAction;
-
- if (oldStatus === 'unsubscribed') {
- [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
- } else {
- [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
- }
-
- $span.toggleClass('hidden');
- $btn.removeClass('disabled');
-
- this.$buttons.attr('data-status', newStatus);
- this.$buttons.find('> span').text(newAction);
-
- for (const button of this.$buttons) {
- const $button = $(button);
-
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
- }
- }
- });
- }
- }
-
- global.ProjectLabelSubscription = ProjectLabelSubscription;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 3aa6f6771ce..e9927c1bf51 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -101,4 +101,4 @@
return ProjectNew;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 7b5e9953598..f80e765ce30 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -101,4 +101,4 @@
return ProjectSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index aad130cf267..3a51c1f26ac 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -6,6 +6,6 @@
return ProjectShow;
})();
-}).call(this);
+}).call(window);
// I kept class for future
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 69a11dfaf39..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(this);
+ }
+}
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 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
new file mode 100644
index 00000000000..5cf28aa7a73
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -0,0 +1,80 @@
+/* eslint-disable comma-dangle, no-unused-vars */
+
+class ProtectedBranchDropdown {
+ constructor(options) {
+ this.onSelect = options.onSelect;
+ this.$dropdown = options.$dropdown;
+ this.$dropdownContainer = this.$dropdown.parent();
+ this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
+ this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+
+ this.buildDropdown();
+ this.bindEvents();
+
+ // Hide footer
+ this.$dropdownFooter.addClass('hidden');
+ }
+
+ buildDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.getProtectedBranches.bind(this),
+ filterable: true,
+ remote: false,
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ toggleLabel(selected) {
+ return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
+ },
+ fieldName: 'protected_branch[name]',
+ text(protectedBranch) {
+ return _.escape(protectedBranch.title);
+ },
+ id(protectedBranch) {
+ return _.escape(protectedBranch.id);
+ },
+ onFilter: this.toggleCreateNewButton.bind(this),
+ clicked: (item, $el, e) => {
+ e.preventDefault();
+ this.onSelect();
+ }
+ });
+ }
+
+ bindEvents() {
+ this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
+ }
+
+ onClickCreateWildcard() {
+ // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
+ this.$dropdown.data('glDropdown').remote.execute();
+ this.$dropdown.data('glDropdown').selectRowAtIndex();
+ }
+
+ getProtectedBranches(term, callback) {
+ if (this.selectedBranch) {
+ callback(gon.open_branches.concat(this.selectedBranch));
+ } else {
+ callback(gon.open_branches);
+ }
+ }
+
+ toggleCreateNewButton(branchName) {
+ this.selectedBranch = {
+ title: branchName,
+ id: branchName,
+ text: branchName
+ };
+
+ if (branchName) {
+ this.$dropdownContainer
+ .find('.create-new-protected-branch code')
+ .text(branchName);
+ }
+
+ this.$dropdownFooter.toggleClass('hidden', !branchName);
+ }
+}
+
+window.ProtectedBranchDropdown = ProtectedBranchDropdown;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
deleted file mode 100644
index 03f4531abf5..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ /dev/null
@@ -1,80 +0,0 @@
-/* eslint-disable comma-dangle, no-unused-vars */
-
-class ProtectedBranchDropdown {
- constructor(options) {
- this.onSelect = options.onSelect;
- this.$dropdown = options.$dropdown;
- this.$dropdownContainer = this.$dropdown.parent();
- this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
-
- this.buildDropdown();
- this.bindEvents();
-
- // Hide footer
- this.$dropdownFooter.addClass('hidden');
- }
-
- buildDropdown() {
- this.$dropdown.glDropdown({
- data: this.getProtectedBranches.bind(this),
- filterable: true,
- remote: false,
- search: {
- fields: ['title']
- },
- selectable: true,
- toggleLabel(selected) {
- return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
- },
- fieldName: 'protected_branch[name]',
- text(protectedBranch) {
- return _.escape(protectedBranch.title);
- },
- id(protectedBranch) {
- return _.escape(protectedBranch.id);
- },
- onFilter: this.toggleCreateNewButton.bind(this),
- clicked: (item, $el, e) => {
- e.preventDefault();
- this.onSelect();
- }
- });
- }
-
- bindEvents() {
- this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
- }
-
- onClickCreateWildcard() {
- // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
- this.$dropdown.data('glDropdown').remote.execute();
- this.$dropdown.data('glDropdown').selectRowAtIndex(0);
- }
-
- getProtectedBranches(term, callback) {
- if (this.selectedBranch) {
- callback(gon.open_branches.concat(this.selectedBranch));
- } else {
- callback(gon.open_branches);
- }
- }
-
- toggleCreateNewButton(branchName) {
- this.selectedBranch = {
- title: branchName,
- id: branchName,
- text: branchName
- };
-
- if (branchName) {
- this.$dropdownContainer
- .find('.create-new-protected-branch code')
- .text(branchName);
- }
-
- this.$dropdownFooter.toggleClass('hidden', !branchName);
- }
-}
-
-window.ProtectedBranchDropdown = ProtectedBranchDropdown;
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 15b3affd469..849c1e31623 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1 +1,5 @@
-/*= require_tree . */
+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/render_gfm.js b/app/assets/javascripts/render_gfm.js
index 0caf8ba4344..48cae8a4fa9 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -9,7 +9,7 @@
this.find('.js-render-math').renderMath();
};
- $(document).on('ready page:load', function() {
+ $(document).on('ready load', function() {
return $('body').renderGFM();
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 6cef449babf..76c61c001ba 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -51,4 +51,4 @@
});
}
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 76a0f993ea0..903862cac6b 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -21,11 +21,16 @@
};
Sidebar.prototype.addEventListeners = function() {
+ const $document = $(document);
+ const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
+
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
+ $(window).on('resize', () => throttledSetSidebarHeight());
+ $document.on('scroll', () => throttledSetSidebarHeight());
+ $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
@@ -191,6 +196,17 @@
}
};
+ Sidebar.prototype.setSidebarHeight = function() {
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const $rightSidebar = $('.js-right-sidebar');
+ const diff = $navHeight - $('body').scrollTop();
+ if (diff > 0) {
+ $rightSidebar.outerHeight($(window).height() - diff);
+ } else {
+ $rightSidebar.outerHeight('100%');
+ }
+ };
+
Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded');
};
@@ -201,4 +217,4 @@
return Sidebar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index b1c0dc37b4d..e66418beeab 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -97,4 +97,4 @@
return Search;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
new file mode 100644
index 00000000000..6fd5345a0a6
--- /dev/null
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -0,0 +1,432 @@
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+
+((global) => {
+ const KEYCODE = {
+ ESCAPE: 27,
+ BACKSPACE: 8,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
+ };
+
+ class SearchAutocomplete {
+ constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+ this.bindEventContext();
+ this.wrap = wrap || $('.search');
+ this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+ this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
+ this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
+ this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
+ this.dropdown = this.wrap.find('.dropdown');
+ this.dropdownContent = this.dropdown.find('.dropdown-content');
+ this.locationBadgeEl = this.getElement('.location-badge');
+ this.scopeInputEl = this.getElement('#scope');
+ this.searchInput = this.getElement('.search-input');
+ this.projectInputEl = this.getElement('#search_project_id');
+ this.groupInputEl = this.getElement('#group_id');
+ this.searchCodeInputEl = this.getElement('#search_code');
+ this.repositoryInputEl = this.getElement('#repository_ref');
+ this.clearInput = this.getElement('.js-clear-input');
+ this.saveOriginalState();
+ // Only when user is logged in
+ if (gon.current_user_id) {
+ this.createAutocomplete();
+ }
+ this.searchInput.addClass('disabled');
+ this.saveTextLength();
+ this.bindEvents();
+ }
+
+ // Finds an element inside wrapper element
+ bindEventContext() {
+ this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
+ this.onClearInputClick = this.onClearInputClick.bind(this);
+ this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
+ this.onSearchInputClick = this.onSearchInputClick.bind(this);
+ this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
+ this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ }
+ getElement(selector) {
+ return this.wrap.find(selector);
+ }
+
+ saveOriginalState() {
+ return this.originalState = this.serializeState();
+ }
+
+ saveTextLength() {
+ return this.lastTextLength = this.searchInput.val().length;
+ }
+
+ createAutocomplete() {
+ return this.searchInput.glDropdown({
+ filterInputBlur: false,
+ filterable: true,
+ filterRemote: true,
+ highlight: true,
+ enterCallback: false,
+ filterInput: 'input#search',
+ search: {
+ fields: ['text']
+ },
+ id: this.getSearchText,
+ data: this.getData.bind(this),
+ selectable: true,
+ clicked: this.onClick.bind(this)
+ });
+ }
+
+ getSearchText(selectedObject, el) {
+ return selectedObject.id ? selectedObject.text : '';
+ }
+
+ getData(term, callback) {
+ var _this, contents, jqXHR;
+ _this = this;
+ if (!term) {
+ if (contents = this.getCategoryContents()) {
+ this.searchInput.data('glDropdown').filter.options.callback(contents);
+ this.enableAutocomplete();
+ }
+ return;
+ }
+ // Prevent multiple ajax calls
+ if (this.loadingSuggestions) {
+ return;
+ }
+ this.loadingSuggestions = true;
+ return jqXHR = $.get(this.autocompletePath, {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term: term
+ }, function(response) {
+ var data, firstCategory, i, lastCategory, len, suggestion;
+ // Hide dropdown menu if no suggestions returns
+ if (!response.length) {
+ _this.disableAutocomplete();
+ return;
+ }
+ data = [];
+ // List results
+ firstCategory = true;
+ for (i = 0, len = response.length; i < len; i += 1) {
+ suggestion = response[i];
+ // Add group header before list each group
+ if (lastCategory !== suggestion.category) {
+ if (!firstCategory) {
+ data.push('separator');
+ }
+ if (firstCategory) {
+ firstCategory = false;
+ }
+ data.push({
+ header: suggestion.category
+ });
+ lastCategory = suggestion.category;
+ }
+ data.push({
+ id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url
+ });
+ }
+ // Add option to proceed with the search
+ if (data.length) {
+ data.push('separator');
+ data.push({
+ text: "Result name contains \"" + term + "\"",
+ url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val())
+ });
+ }
+ return callback(data);
+ }).always(function() {
+ return _this.loadingSuggestions = false;
+ });
+ }
+
+ getCategoryContents() {
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
+ userId = gon.current_user_id;
+ userName = gon.current_username;
+ utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
+ if (utils.isInGroupsPage() && groupOptions) {
+ options = groupOptions[utils.getGroupSlug()];
+ } else if (utils.isInProjectPage() && projectOptions) {
+ options = projectOptions[utils.getProjectSlug()];
+ } else if (dashboardOptions) {
+ options = dashboardOptions;
+ }
+ issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name;
+ items = [
+ {
+ header: "" + name
+ }, {
+ text: 'Issues assigned to me',
+ url: issuesPath + "/?assignee_username=" + userName
+ }, {
+ text: "Issues I've created",
+ url: issuesPath + "/?author_username=" + userName
+ }, 'separator', {
+ text: 'Merge requests assigned to me',
+ url: mrPath + "/?assignee_username=" + userName
+ }, {
+ text: "Merge requests I've created",
+ url: mrPath + "/?author_username=" + userName
+ }
+ ];
+ if (!name) {
+ items.splice(0, 1);
+ }
+ return items;
+ }
+
+ serializeState() {
+ return {
+ // Search Criteria
+ search_project_id: this.projectInputEl.val(),
+ group_id: this.groupInputEl.val(),
+ search_code: this.searchCodeInputEl.val(),
+ repository_ref: this.repositoryInputEl.val(),
+ scope: this.scopeInputEl.val(),
+ // Location badge
+ _location: this.locationBadgeEl.text()
+ };
+ }
+
+ bindEvents() {
+ this.searchInput.on('keydown', this.onSearchInputKeyDown);
+ this.searchInput.on('keyup', this.onSearchInputKeyUp);
+ this.searchInput.on('click', this.onSearchInputClick);
+ this.searchInput.on('focus', this.onSearchInputFocus);
+ this.searchInput.on('blur', this.onSearchInputBlur);
+ this.clearInput.on('click', this.onClearInputClick);
+ return this.locationBadgeEl.on('click', (function(_this) {
+ return function() {
+ return _this.searchInput.focus();
+ };
+ })(this));
+ }
+
+ enableAutocomplete() {
+ var _this;
+ // No need to enable anything if user is not logged in
+ if (!gon.current_user_id) {
+ return;
+ }
+ if (!this.dropdown.hasClass('open')) {
+ _this = this;
+ this.loadingSuggestions = false;
+ this.dropdown.addClass('open').trigger('shown.bs.dropdown');
+ return this.searchInput.removeClass('disabled');
+ }
+ }
+
+ // Saves last length of the entered text
+ onSearchInputKeyDown() {
+ return this.saveTextLength();
+ }
+
+ onSearchInputKeyUp(e) {
+ switch (e.keyCode) {
+ case KEYCODE.BACKSPACE:
+ // when trying to remove the location badge
+ if (this.lastTextLength === 0 && this.badgePresent()) {
+ this.removeLocationBadge();
+ }
+ // When removing the last character and no badge is present
+ if (this.lastTextLength === 1) {
+ this.disableAutocomplete();
+ }
+ // When removing any character from existin value
+ if (this.lastTextLength > 1) {
+ this.enableAutocomplete();
+ }
+ break;
+ case KEYCODE.ESCAPE:
+ this.restoreOriginalState();
+ break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
+ default:
+ // Handle the case when deleting the input value other than backspace
+ // e.g. Pressing ctrl + backspace or ctrl + x
+ if (this.searchInput.val() === '') {
+ this.disableAutocomplete();
+ } else {
+ // We should display the menu only when input is not empty
+ if (e.keyCode !== KEYCODE.ENTER) {
+ this.enableAutocomplete();
+ }
+ }
+ }
+ this.wrap.toggleClass('has-value', !!e.target.value);
+ }
+
+ // Avoid falsy value to be returned
+ onSearchInputClick(e) {
+ return e.stopImmediatePropagation();
+ }
+
+ onSearchInputFocus() {
+ this.isFocused = true;
+ this.wrap.addClass('search-active');
+ if (this.getValue() === '') {
+ return this.getData();
+ }
+ }
+
+ getValue() {
+ return this.searchInput.val();
+ }
+
+ onClearInputClick(e) {
+ e.preventDefault();
+ return this.searchInput.val('').focus();
+ }
+
+ onSearchInputBlur(e) {
+ this.isFocused = false;
+ this.wrap.removeClass('search-active');
+ // If input is blank then restore state
+ if (this.searchInput.val() === '') {
+ return this.restoreOriginalState();
+ }
+ }
+
+ addLocationBadge(item) {
+ var badgeText, category, value;
+ category = item.category != null ? item.category + ": " : '';
+ value = item.value != null ? item.value : '';
+ badgeText = "" + category + value;
+ this.locationBadgeEl.text(badgeText).show();
+ return this.wrap.addClass('has-location-badge');
+ }
+
+ hasLocationBadge() {
+ return this.wrap.is('.has-location-badge');
+ }
+
+ restoreOriginalState() {
+ var i, input, inputs, len;
+ inputs = Object.keys(this.originalState);
+ for (i = 0, len = inputs.length; i < len; i += 1) {
+ input = inputs[i];
+ this.getElement("#" + input).val(this.originalState[input]);
+ }
+ if (this.originalState._location === '') {
+ return this.locationBadgeEl.hide();
+ } else {
+ return this.addLocationBadge({
+ value: this.originalState._location
+ });
+ }
+ }
+
+ badgePresent() {
+ return this.locationBadgeEl.length;
+ }
+
+ resetSearchState() {
+ var i, input, inputs, len, results;
+ inputs = Object.keys(this.originalState);
+ results = [];
+ for (i = 0, len = inputs.length; i < len; i += 1) {
+ input = inputs[i];
+ // _location isnt a input
+ if (input === '_location') {
+ break;
+ }
+ results.push(this.getElement("#" + input).val(''));
+ }
+ return results;
+ }
+
+ removeLocationBadge() {
+ this.locationBadgeEl.hide();
+ this.resetSearchState();
+ this.wrap.removeClass('has-location-badge');
+ return this.disableAutocomplete();
+ }
+
+ disableAutocomplete() {
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
+ }
+
+ restoreMenu() {
+ var html;
+ html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
+ return this.dropdownContent.html(html);
+ }
+
+ onClick(item, $el, e) {
+ if (location.pathname.indexOf(item.url) !== -1) {
+ if (!e.metaKey) e.preventDefault();
+ if (!this.badgePresent) {
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This project'
+ });
+ }
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This group'
+ });
+ }
+ }
+ $el.removeClass('is-active');
+ this.disableAutocomplete();
+ return this.searchInput.val('').focus();
+ }
+ }
+ }
+
+ global.SearchAutocomplete = SearchAutocomplete;
+
+ $(function() {
+ var $projectOptionsDataEl = $('.js-search-project-options');
+ var $groupOptionsDataEl = $('.js-search-group-options');
+ var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+
+ if ($projectOptionsDataEl.length) {
+ gl.projectOptions = gl.projectOptions || {};
+
+ var projectPath = $projectOptionsDataEl.data('project-path');
+
+ gl.projectOptions[projectPath] = {
+ name: $projectOptionsDataEl.data('name'),
+ issuesPath: $projectOptionsDataEl.data('issues-path'),
+ mrPath: $projectOptionsDataEl.data('mr-path')
+ };
+ }
+
+ if ($groupOptionsDataEl.length) {
+ gl.groupOptions = gl.groupOptions || {};
+
+ var groupPath = $groupOptionsDataEl.data('group-path');
+
+ gl.groupOptions[groupPath] = {
+ name: $groupOptionsDataEl.data('name'),
+ issuesPath: $groupOptionsDataEl.data('issues-path'),
+ mrPath: $groupOptionsDataEl.data('mr-path')
+ };
+ }
+
+ if ($dashboardOptionsDataEl.length) {
+ gl.dashboardOptions = {
+ issuesPath: $dashboardOptionsDataEl.data('issues-path'),
+ mrPath: $dashboardOptionsDataEl.data('mr-path')
+ };
+ }
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
deleted file mode 100644
index 6250e75d407..00000000000
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ /dev/null
@@ -1,432 +0,0 @@
-/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
-
-((global) => {
- const KEYCODE = {
- ESCAPE: 27,
- BACKSPACE: 8,
- ENTER: 13,
- UP: 38,
- DOWN: 40
- };
-
- class SearchAutocomplete {
- constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
- this.bindEventContext();
- this.wrap = wrap || $('.search');
- this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
- this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
- this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
- this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
- this.dropdown = this.wrap.find('.dropdown');
- this.dropdownContent = this.dropdown.find('.dropdown-content');
- this.locationBadgeEl = this.getElement('.location-badge');
- this.scopeInputEl = this.getElement('#scope');
- this.searchInput = this.getElement('.search-input');
- this.projectInputEl = this.getElement('#search_project_id');
- this.groupInputEl = this.getElement('#group_id');
- this.searchCodeInputEl = this.getElement('#search_code');
- this.repositoryInputEl = this.getElement('#repository_ref');
- this.clearInput = this.getElement('.js-clear-input');
- this.saveOriginalState();
- // Only when user is logged in
- if (gon.current_user_id) {
- this.createAutocomplete();
- }
- this.searchInput.addClass('disabled');
- this.saveTextLength();
- this.bindEvents();
- }
-
- // Finds an element inside wrapper element
- bindEventContext() {
- this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
- this.onClearInputClick = this.onClearInputClick.bind(this);
- this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
- this.onSearchInputClick = this.onSearchInputClick.bind(this);
- this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
- this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
- }
- getElement(selector) {
- return this.wrap.find(selector);
- }
-
- saveOriginalState() {
- return this.originalState = this.serializeState();
- }
-
- saveTextLength() {
- return this.lastTextLength = this.searchInput.val().length;
- }
-
- createAutocomplete() {
- return this.searchInput.glDropdown({
- filterInputBlur: false,
- filterable: true,
- filterRemote: true,
- highlight: true,
- enterCallback: false,
- filterInput: 'input#search',
- search: {
- fields: ['text']
- },
- id: this.getSearchText,
- data: this.getData.bind(this),
- selectable: true,
- clicked: this.onClick.bind(this)
- });
- }
-
- getSearchText(selectedObject, el) {
- return selectedObject.id ? selectedObject.text : '';
- }
-
- getData(term, callback) {
- var _this, contents, jqXHR;
- _this = this;
- if (!term) {
- if (contents = this.getCategoryContents()) {
- this.searchInput.data('glDropdown').filter.options.callback(contents);
- this.enableAutocomplete();
- }
- return;
- }
- // Prevent multiple ajax calls
- if (this.loadingSuggestions) {
- return;
- }
- this.loadingSuggestions = true;
- return jqXHR = $.get(this.autocompletePath, {
- project_id: this.projectId,
- project_ref: this.projectRef,
- term: term
- }, function(response) {
- var data, firstCategory, i, lastCategory, len, suggestion;
- // Hide dropdown menu if no suggestions returns
- if (!response.length) {
- _this.disableAutocomplete();
- return;
- }
- data = [];
- // List results
- firstCategory = true;
- for (i = 0, len = response.length; i < len; i += 1) {
- suggestion = response[i];
- // Add group header before list each group
- if (lastCategory !== suggestion.category) {
- if (!firstCategory) {
- data.push('separator');
- }
- if (firstCategory) {
- firstCategory = false;
- }
- data.push({
- header: suggestion.category
- });
- lastCategory = suggestion.category;
- }
- data.push({
- id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
- category: suggestion.category,
- text: suggestion.label,
- url: suggestion.url
- });
- }
- // Add option to proceed with the search
- if (data.length) {
- data.push('separator');
- data.push({
- text: "Result name contains \"" + term + "\"",
- url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val())
- });
- }
- return callback(data);
- }).always(function() {
- return _this.loadingSuggestions = false;
- });
- }
-
- getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
- userId = gon.current_user_id;
- userName = gon.current_username;
- utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
- if (utils.isInGroupsPage() && groupOptions) {
- options = groupOptions[utils.getGroupSlug()];
- } else if (utils.isInProjectPage() && projectOptions) {
- options = projectOptions[utils.getProjectSlug()];
- } else if (dashboardOptions) {
- options = dashboardOptions;
- }
- issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name;
- items = [
- {
- header: "" + name
- }, {
- text: 'Issues assigned to me',
- url: issuesPath + "/?assignee_username=" + userName
- }, {
- text: "Issues I've created",
- url: issuesPath + "/?author_username=" + userName
- }, 'separator', {
- text: 'Merge requests assigned to me',
- url: mrPath + "/?assignee_id=" + userId
- }, {
- text: "Merge requests I've created",
- url: mrPath + "/?author_id=" + userId
- }
- ];
- if (!name) {
- items.splice(0, 1);
- }
- return items;
- }
-
- serializeState() {
- return {
- // Search Criteria
- search_project_id: this.projectInputEl.val(),
- group_id: this.groupInputEl.val(),
- search_code: this.searchCodeInputEl.val(),
- repository_ref: this.repositoryInputEl.val(),
- scope: this.scopeInputEl.val(),
- // Location badge
- _location: this.locationBadgeEl.text()
- };
- }
-
- bindEvents() {
- this.searchInput.on('keydown', this.onSearchInputKeyDown);
- this.searchInput.on('keyup', this.onSearchInputKeyUp);
- this.searchInput.on('click', this.onSearchInputClick);
- this.searchInput.on('focus', this.onSearchInputFocus);
- this.searchInput.on('blur', this.onSearchInputBlur);
- this.clearInput.on('click', this.onClearInputClick);
- return this.locationBadgeEl.on('click', (function(_this) {
- return function() {
- return _this.searchInput.focus();
- };
- })(this));
- }
-
- enableAutocomplete() {
- var _this;
- // No need to enable anything if user is not logged in
- if (!gon.current_user_id) {
- return;
- }
- if (!this.dropdown.hasClass('open')) {
- _this = this;
- this.loadingSuggestions = false;
- this.dropdown.addClass('open').trigger('shown.bs.dropdown');
- return this.searchInput.removeClass('disabled');
- }
- }
-
- // Saves last length of the entered text
- onSearchInputKeyDown() {
- return this.saveTextLength();
- }
-
- onSearchInputKeyUp(e) {
- switch (e.keyCode) {
- case KEYCODE.BACKSPACE:
- // when trying to remove the location badge
- if (this.lastTextLength === 0 && this.badgePresent()) {
- this.removeLocationBadge();
- }
- // When removing the last character and no badge is present
- if (this.lastTextLength === 1) {
- this.disableAutocomplete();
- }
- // When removing any character from existin value
- if (this.lastTextLength > 1) {
- this.enableAutocomplete();
- }
- break;
- case KEYCODE.ESCAPE:
- this.restoreOriginalState();
- break;
- case KEYCODE.ENTER:
- this.disableAutocomplete();
- break;
- case KEYCODE.UP:
- case KEYCODE.DOWN:
- return;
- default:
- // Handle the case when deleting the input value other than backspace
- // e.g. Pressing ctrl + backspace or ctrl + x
- if (this.searchInput.val() === '') {
- this.disableAutocomplete();
- } else {
- // We should display the menu only when input is not empty
- if (e.keyCode !== KEYCODE.ENTER) {
- this.enableAutocomplete();
- }
- }
- }
- this.wrap.toggleClass('has-value', !!e.target.value);
- }
-
- // Avoid falsy value to be returned
- onSearchInputClick(e) {
- return e.stopImmediatePropagation();
- }
-
- onSearchInputFocus() {
- this.isFocused = true;
- this.wrap.addClass('search-active');
- if (this.getValue() === '') {
- return this.getData();
- }
- }
-
- getValue() {
- return this.searchInput.val();
- }
-
- onClearInputClick(e) {
- e.preventDefault();
- return this.searchInput.val('').focus();
- }
-
- onSearchInputBlur(e) {
- this.isFocused = false;
- this.wrap.removeClass('search-active');
- // If input is blank then restore state
- if (this.searchInput.val() === '') {
- return this.restoreOriginalState();
- }
- }
-
- addLocationBadge(item) {
- var badgeText, category, value;
- category = item.category != null ? item.category + ": " : '';
- value = item.value != null ? item.value : '';
- badgeText = "" + category + value;
- this.locationBadgeEl.text(badgeText).show();
- return this.wrap.addClass('has-location-badge');
- }
-
- hasLocationBadge() {
- return this.wrap.is('.has-location-badge');
- }
-
- restoreOriginalState() {
- var i, input, inputs, len;
- inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
- this.getElement("#" + input).val(this.originalState[input]);
- }
- if (this.originalState._location === '') {
- return this.locationBadgeEl.hide();
- } else {
- return this.addLocationBadge({
- value: this.originalState._location
- });
- }
- }
-
- badgePresent() {
- return this.locationBadgeEl.length;
- }
-
- resetSearchState() {
- var i, input, inputs, len, results;
- inputs = Object.keys(this.originalState);
- results = [];
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
- // _location isnt a input
- if (input === '_location') {
- break;
- }
- results.push(this.getElement("#" + input).val(''));
- }
- return results;
- }
-
- removeLocationBadge() {
- this.locationBadgeEl.hide();
- this.resetSearchState();
- this.wrap.removeClass('has-location-badge');
- return this.disableAutocomplete();
- }
-
- disableAutocomplete() {
- if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
- this.restoreMenu();
- }
- }
-
- restoreMenu() {
- var html;
- html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
- return this.dropdownContent.html(html);
- }
-
- onClick(item, $el, e) {
- if (location.pathname.indexOf(item.url) !== -1) {
- if (!e.metaKey) e.preventDefault();
- if (!this.badgePresent) {
- if (item.category === 'Projects') {
- this.projectInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This project'
- });
- }
- if (item.category === 'Groups') {
- this.groupInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This group'
- });
- }
- }
- $el.removeClass('is-active');
- this.disableAutocomplete();
- return this.searchInput.val('').focus();
- }
- }
- }
-
- global.SearchAutocomplete = SearchAutocomplete;
-
- $(function() {
- var $projectOptionsDataEl = $('.js-search-project-options');
- var $groupOptionsDataEl = $('.js-search-group-options');
- var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
-
- if ($projectOptionsDataEl.length) {
- gl.projectOptions = gl.projectOptions || {};
-
- var projectPath = $projectOptionsDataEl.data('project-path');
-
- gl.projectOptions[projectPath] = {
- name: $projectOptionsDataEl.data('name'),
- issuesPath: $projectOptionsDataEl.data('issues-path'),
- mrPath: $projectOptionsDataEl.data('mr-path')
- };
- }
-
- if ($groupOptionsDataEl.length) {
- gl.groupOptions = gl.groupOptions || {};
-
- var groupPath = $groupOptionsDataEl.data('group-path');
-
- gl.groupOptions[groupPath] = {
- name: $groupOptionsDataEl.data('name'),
- issuesPath: $groupOptionsDataEl.data('issues-path'),
- mrPath: $groupOptionsDataEl.data('mr-path')
- };
- }
-
- if ($dashboardOptionsDataEl.length) {
- gl.dashboardOptions = {
- issuesPath: $dashboardOptionsDataEl.data('issues-path'),
- mrPath: $dashboardOptionsDataEl.data('mr-path')
- };
- }
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index c56ee429b8e..81766f4bd55 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
-/* global Turbolinks */
/* global findFileURL */
(function() {
@@ -23,7 +22,7 @@
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
- return Turbolinks.visit(findFileURL);
+ return gl.utils.visitUrl(findFileURL);
});
}
}
@@ -98,4 +97,4 @@
}
};
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index d50ddd98de1..bfe90aef71e 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,29 +1,29 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
-/* global Shortcuts */
/* global Mousetrap */
+/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
-(function() {
- var 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;
+const defaults = {
+ skipResetBindings: false,
+ fileBlobPermalinkUrl: null,
+};
- this.ShortcutsBlob = (function(superClass) {
- extend(ShortcutsBlob, superClass);
+class ShortcutsBlob extends Shortcuts {
+ constructor(opts) {
+ const options = Object.assign({}, defaults, opts);
+ super(options.skipResetBindings);
+ this.options = options;
- function ShortcutsBlob(skipResetBindings) {
- ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
- Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
- }
+ Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
+ }
- ShortcutsBlob.copyToClipboard = function() {
- var clipboardButton;
- clipboardButton = $('.btn-clipboard');
- if (clipboardButton) {
- return clipboardButton.click();
- }
- };
+ moveToFilePermalink() {
+ if (this.options.fileBlobPermalinkUrl) {
+ const hash = gl.utils.getLocationHash();
+ const hashUrlString = hash ? `#${hash}` : '';
+ gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+ }
+ }
+}
- return ShortcutsBlob;
- })(Shortcuts);
-}).call(this);
+module.exports = ShortcutsBlob;
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 603fefbf15a..e7baea894f6 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var 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; },
@@ -37,4 +37,4 @@
return ShortcutsDashboardNavigation;
})(Shortcuts);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 8469837533b..a27ac264a5c 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var 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; },
@@ -35,4 +35,4 @@
return ShortcutsFindFile;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 4dcc5ebe28f..fe58e98cee5 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,11 +1,10 @@
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
-/* global Turbolinks */
/* global ShortcutsNavigation */
/* global sidebar */
-/*= require mousetrap */
-/*= require shortcuts_navigation */
+require('mousetrap');
+require('./shortcuts_navigation');
(function() {
var 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; },
@@ -80,7 +79,7 @@
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
- return Turbolinks.visit($editBtn.attr('href'));
+ return gl.utils.visitUrl($editBtn.attr('href'));
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
@@ -90,4 +89,4 @@
return ShortcutsIssuable;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index afeda0dd5fe..09a58cad2b2 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var 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; },
@@ -32,7 +32,7 @@
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');
@@ -65,4 +65,4 @@
return ShortcutsNavigation;
})(Shortcuts);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 79896e35cbb..4c2bf8bf001 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var 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; },
@@ -25,4 +25,4 @@
return ShortcutsNetwork;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
deleted file mode 100644
index 05234643c18..00000000000
--- a/app/assets/javascripts/sidebar.js.es6
+++ /dev/null
@@ -1,97 +0,0 @@
-/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
-/* global Cookies */
-
-((global) => {
- let singleton;
-
- const pinnedStateCookie = 'pin_nav';
- const sidebarBreakpoint = 1024;
-
- const pageSelector = '.page-with-sidebar';
- const navbarSelector = '.navbar-fixed-top';
- const sidebarWrapperSelector = '.sidebar-wrapper';
- const sidebarContentSelector = '.nav-sidebar';
-
- const pinnedToggleSelector = '.js-nav-pin';
- const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
-
- const pinnedPageClass = 'page-sidebar-pinned';
- const expandedPageClass = 'page-sidebar-expanded';
-
- const pinnedNavbarClass = 'header-sidebar-pinned';
- const expandedNavbarClass = 'header-sidebar-expanded';
-
- class Sidebar {
- constructor() {
- if (!singleton) {
- singleton = this;
- singleton.init();
- }
- return singleton;
- }
-
- init() {
- this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
- this.isExpanded = (
- window.innerWidth >= sidebarBreakpoint &&
- $(pageSelector).hasClass(expandedPageClass)
- );
- $(document)
- .on('click', sidebarToggleSelector, () => this.toggleSidebar())
- .on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body', (e) => this.handleClickEvent(e))
- .on('page:change', () => this.renderState())
- .on('todo:toggle', (e, count) => this.updateTodoCount(count));
- this.renderState();
- }
-
- handleClickEvent(e) {
- if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
- const $target = $(e.target);
- const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
- const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
- if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
- this.toggleSidebar();
- }
- }
- }
-
- updateTodoCount(count) {
- $('.js-todos-count').text(gl.text.addDelimiter(count));
- }
-
- toggleSidebar() {
- this.isExpanded = !this.isExpanded;
- this.renderState();
- }
-
- togglePinnedState() {
- this.isPinned = !this.isPinned;
- if (!this.isPinned) {
- this.isExpanded = false;
- }
- Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
- this.renderState();
- }
-
- renderState() {
- $(pageSelector)
- .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
- .toggleClass(expandedPageClass, this.isExpanded);
- $(navbarSelector)
- .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
- .toggleClass(expandedNavbarClass, this.isExpanded);
-
- const $pinnedToggle = $(pinnedToggleSelector);
- const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
- const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
- $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
-
- if (this.isExpanded) {
- setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
- }
- }
- }
-
- global.Sidebar = Sidebar;
-})(window.gl || (window.gl = {}));
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/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 5b20c63384c..294d087554e 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -33,13 +33,13 @@
this.$toggleIcon.addClass('fa-caret-down');
}
- $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff($(e.target));
}).bind(this));
}
SingleFileDiff.prototype.toggleDiff = function($target, cb) {
- if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
+ if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
@@ -95,4 +95,4 @@
}
});
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
new file mode 100644
index 00000000000..d1bdc353be2
--- /dev/null
+++ b/app/assets/javascripts/smart_interval.js
@@ -0,0 +1,158 @@
+/*
+* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
+* and controllable by a public API.
+*
+* */
+
+(() => {
+ class SmartInterval {
+ /**
+ * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
+ * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
+ * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
+ * when the page is hidden
+ * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
+ * @param { boolean } opts.lazyStart Configure if timer is initialized on
+ * instantiation or lazily
+ * @param { boolean } opts.immediateExecution Configure if callback should
+ * be executed before the first interval.
+ */
+ constructor(opts = {}) {
+ this.cfg = {
+ callback: opts.callback,
+ startingInterval: opts.startingInterval,
+ maxInterval: opts.maxInterval,
+ hiddenInterval: opts.hiddenInterval,
+ incrementByFactorOf: opts.incrementByFactorOf,
+ lazyStart: opts.lazyStart,
+ immediateExecution: opts.immediateExecution,
+ };
+
+ this.state = {
+ intervalId: null,
+ currentInterval: this.cfg.startingInterval,
+ pageVisibility: 'visible',
+ };
+
+ this.initInterval();
+ }
+ /* public */
+
+ start() {
+ const cfg = this.cfg;
+ const state = this.state;
+
+ if (cfg.immediateExecution) {
+ cfg.immediateExecution = false;
+ cfg.callback();
+ }
+
+ state.intervalId = window.setInterval(() => {
+ cfg.callback();
+
+ if (this.getCurrentInterval() === cfg.maxInterval) {
+ return;
+ }
+
+ this.incrementInterval();
+ this.resume();
+ }, this.getCurrentInterval());
+ }
+
+ // cancel the existing timer, setting the currentInterval back to startingInterval
+ cancel() {
+ this.setCurrentInterval(this.cfg.startingInterval);
+ this.stopTimer();
+ }
+
+ onVisibilityHidden() {
+ if (this.cfg.hiddenInterval) {
+ this.setCurrentInterval(this.cfg.hiddenInterval);
+ this.resume();
+ } else {
+ this.cancel();
+ }
+ }
+
+ // start a timer, using the existing interval
+ resume() {
+ this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.start();
+ }
+
+ onVisibilityVisible() {
+ this.cancel();
+ this.start();
+ }
+
+ destroy() {
+ this.cancel();
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+ $(document).off('visibilitychange').off('beforeunload');
+ }
+
+ /* private */
+
+ initInterval() {
+ const cfg = this.cfg;
+
+ if (!cfg.lazyStart) {
+ this.start();
+ }
+
+ this.initVisibilityChangeHandling();
+ this.initPageUnloadHandling();
+ }
+
+ initVisibilityChangeHandling() {
+ // cancel interval when tab no longer shown (prevents cached pages from polling)
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
+ }
+
+ initPageUnloadHandling() {
+ // TODO: Consider refactoring in light of turbolinks removal.
+ // prevent interval continuing after page change, when kept in cache by Turbolinks
+ $(document).on('beforeunload', () => this.cancel());
+ }
+
+ handleVisibilityChange(e) {
+ this.state.pageVisibility = e.target.visibilityState;
+ const intervalAction = this.isPageVisible() ?
+ this.onVisibilityVisible :
+ this.onVisibilityHidden;
+
+ intervalAction.apply(this);
+ }
+
+ getCurrentInterval() {
+ return this.state.currentInterval;
+ }
+
+ setCurrentInterval(newInterval) {
+ this.state.currentInterval = newInterval;
+ }
+
+ incrementInterval() {
+ const cfg = this.cfg;
+ const currentInterval = this.getCurrentInterval();
+ if (cfg.hiddenInterval && !this.isPageVisible()) return;
+ let nextInterval = currentInterval * cfg.incrementByFactorOf;
+
+ if (nextInterval > cfg.maxInterval) {
+ nextInterval = cfg.maxInterval;
+ }
+
+ this.setCurrentInterval(nextInterval);
+ }
+
+ isPageVisible() { return this.state.pageVisibility === 'visible'; }
+
+ stopTimer() {
+ const state = this.state;
+
+ state.intervalId = window.clearInterval(state.intervalId);
+ }
+ }
+ gl.SmartInterval = SmartInterval;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
deleted file mode 100644
index 40f67637c7c..00000000000
--- a/app/assets/javascripts/smart_interval.js.es6
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
-* and controllable by a public API.
-*
-* */
-
-(() => {
- class SmartInterval {
- /**
- * @param { function } opts.callback Function to be called on each iteration (required)
- * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
- * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
- * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
- * when the page is hidden
- * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
- * @param { boolean } opts.lazyStart Configure if timer is initialized on
- * instantiation or lazily
- * @param { boolean } opts.immediateExecution Configure if callback should
- * be executed before the first interval.
- */
- constructor(opts = {}) {
- this.cfg = {
- callback: opts.callback,
- startingInterval: opts.startingInterval,
- maxInterval: opts.maxInterval,
- hiddenInterval: opts.hiddenInterval,
- incrementByFactorOf: opts.incrementByFactorOf,
- lazyStart: opts.lazyStart,
- immediateExecution: opts.immediateExecution,
- };
-
- this.state = {
- intervalId: null,
- currentInterval: this.cfg.startingInterval,
- pageVisibility: 'visible',
- };
-
- this.initInterval();
- }
- /* public */
-
- start() {
- const cfg = this.cfg;
- const state = this.state;
-
- if (cfg.immediateExecution) {
- cfg.immediateExecution = false;
- cfg.callback();
- }
-
- state.intervalId = window.setInterval(() => {
- cfg.callback();
-
- if (this.getCurrentInterval() === cfg.maxInterval) {
- return;
- }
-
- this.incrementInterval();
- this.resume();
- }, this.getCurrentInterval());
- }
-
- // cancel the existing timer, setting the currentInterval back to startingInterval
- cancel() {
- this.setCurrentInterval(this.cfg.startingInterval);
- this.stopTimer();
- }
-
- onVisibilityHidden() {
- if (this.cfg.hiddenInterval) {
- this.setCurrentInterval(this.cfg.hiddenInterval);
- this.resume();
- } else {
- this.cancel();
- }
- }
-
- // start a timer, using the existing interval
- resume() {
- this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
- this.start();
- }
-
- onVisibilityVisible() {
- this.cancel();
- this.start();
- }
-
- destroy() {
- this.cancel();
- document.removeEventListener('visibilitychange', this.handleVisibilityChange);
- $(document).off('visibilitychange').off('page:before-unload');
- }
-
- /* private */
-
- initInterval() {
- const cfg = this.cfg;
-
- if (!cfg.lazyStart) {
- this.start();
- }
-
- this.initVisibilityChangeHandling();
- this.initPageUnloadHandling();
- }
-
- initVisibilityChangeHandling() {
- // cancel interval when tab no longer shown (prevents cached pages from polling)
- document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
- }
-
- initPageUnloadHandling() {
- // prevent interval continuing after page change, when kept in cache by Turbolinks
- $(document).on('page:before-unload', () => this.cancel());
- }
-
- handleVisibilityChange(e) {
- this.state.pageVisibility = e.target.visibilityState;
- const intervalAction = this.isPageVisible() ?
- this.onVisibilityVisible :
- this.onVisibilityHidden;
-
- intervalAction.apply(this);
- }
-
- getCurrentInterval() {
- return this.state.currentInterval;
- }
-
- setCurrentInterval(newInterval) {
- this.state.currentInterval = newInterval;
- }
-
- incrementInterval() {
- const cfg = this.cfg;
- const currentInterval = this.getCurrentInterval();
- if (cfg.hiddenInterval && !this.isPageVisible()) return;
- let nextInterval = currentInterval * cfg.incrementByFactorOf;
-
- if (nextInterval > cfg.maxInterval) {
- nextInterval = cfg.maxInterval;
- }
-
- this.setCurrentInterval(nextInterval);
- }
-
- isPageVisible() { return this.state.pageVisibility === 'visible'; }
-
- stopTimer() {
- const state = this.state;
-
- state.intervalId = window.clearInterval(state.intervalId);
- }
- }
- gl.SmartInterval = SmartInterval;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index cfb4ff82a73..a98403f4cf2 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,8 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-/*= require_tree . */
-
(function() {
$(function() {
var editor = ace.edit("editor");
@@ -11,4 +9,4 @@
$(".snippet-file-content").val(editor.getValue());
});
});
-}).call(this);
+}).call(window);
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/star.js b/app/assets/javascripts/star.js
index 531fd0e9c32..c75b44cc2fd 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -27,4 +27,4 @@
return Star;
})();
-}).call(this);
+}).call(window);
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/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 187356f0bf9..8b25f43ffc7 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -31,4 +31,4 @@
return SubscriptionSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 115716bff6a..7c063fae045 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -24,4 +24,4 @@
}
}
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
new file mode 100644
index 00000000000..b1402c0a880
--- /dev/null
+++ b/app/assets/javascripts/task_list.js
@@ -0,0 +1,52 @@
+/* global Flash */
+require('vendor/task_list');
+
+class TaskList {
+ constructor(options = {}) {
+ this.selector = options.selector;
+ 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();
+ }
+
+ init() {
+ // Prevent duplicate event bindings
+ this.disable();
+ $(`${this.selector} .js-task-list-container`).taskList('enable');
+ $(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this));
+ }
+
+ disable() {
+ $(`${this.selector} .js-task-list-container`).taskList('disable');
+ $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`);
+ }
+
+ update(e) {
+ const $target = $(e.target);
+ const patchData = {};
+ patchData[this.dataType] = {
+ [this.fieldName]: $target.val(),
+ };
+ return $.ajax({
+ type: 'PATCH',
+ url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
+ data: patchData,
+ success: this.onSuccess,
+ error: this.onError,
+ });
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.TaskList = TaskList;
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
new file mode 100644
index 00000000000..e9e9aafd71a
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -0,0 +1,60 @@
+/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
+/* global Api */
+
+require('../blob/template_selector');
+
+((global) => {
+ class IssuableTemplateSelector extends gl.TemplateSelector {
+ constructor(...args) {
+ super(...args);
+ this.projectPath = this.dropdown.data('project-path');
+ this.namespacePath = this.dropdown.data('namespace-path');
+ this.issuableType = this.wrapper.data('issuable-type');
+ this.titleInput = $(`#${this.issuableType}_title`);
+
+ const initialQuery = {
+ name: this.dropdown.data('selected')
+ };
+
+ if (initialQuery.name) this.requestFile(initialQuery);
+
+ $('.reset-template', this.dropdown.parent()).on('click', () => {
+ this.setInputValueToTemplateContent();
+ });
+
+ $('.no-template', this.dropdown.parent()).on('click', () => {
+ this.currentTemplate.content = '';
+ this.setInputValueToTemplateContent();
+ $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
+ });
+ }
+
+ requestFile(query) {
+ this.startLoadingSpinner();
+ Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ if (err) return; // Error handled by global AJAX error handler
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ });
+ return;
+ }
+
+ setInputValueToTemplateContent() {
+ // `this.requestFileSuccess` sets the value of the description input field
+ // to the content of the template selected.
+ if (this.titleInput.val() === '') {
+ // If the title has not yet been set, focus the title input and
+ // skip focusing the description input by setting `true` as the
+ // `skipFocus` option to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
+ this.titleInput.focus();
+ } else {
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
+ }
+ return;
+ }
+ }
+
+ global.IssuableTemplateSelector = IssuableTemplateSelector;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
deleted file mode 100644
index b0132af70f2..00000000000
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ /dev/null
@@ -1,60 +0,0 @@
-/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
-/* global Api */
-
-/*= require ../blob/template_selector */
-
-((global) => {
- class IssuableTemplateSelector extends gl.TemplateSelector {
- constructor(...args) {
- super(...args);
- this.projectPath = this.dropdown.data('project-path');
- this.namespacePath = this.dropdown.data('namespace-path');
- this.issuableType = this.wrapper.data('issuable-type');
- this.titleInput = $(`#${this.issuableType}_title`);
-
- const initialQuery = {
- name: this.dropdown.data('selected')
- };
-
- if (initialQuery.name) this.requestFile(initialQuery);
-
- $('.reset-template', this.dropdown.parent()).on('click', () => {
- this.setInputValueToTemplateContent();
- });
-
- $('.no-template', this.dropdown.parent()).on('click', () => {
- this.currentTemplate.content = '';
- this.setInputValueToTemplateContent();
- $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
- });
- }
-
- requestFile(query) {
- this.startLoadingSpinner();
- Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
- this.currentTemplate = currentTemplate;
- if (err) return; // Error handled by global AJAX error handler
- this.stopLoadingSpinner();
- this.setInputValueToTemplateContent();
- });
- return;
- }
-
- setInputValueToTemplateContent() {
- // `this.requestFileSuccess` sets the value of the description input field
- // to the content of the template selected.
- if (this.titleInput.val() === '') {
- // If the title has not yet been set, focus the title input and
- // skip focusing the description input by setting `true` as the
- // `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
- this.titleInput.focus();
- } else {
- this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
- }
- return;
- }
- }
-
- global.IssuableTemplateSelector = IssuableTemplateSelector;
-})(window.gl || (window.gl = {}));
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 b/app/assets/javascripts/terminal/terminal_bundle.js
new file mode 100644
index 00000000000..13cf3a10a38
--- /dev/null
+++ b/app/assets/javascripts/terminal/terminal_bundle.js
@@ -0,0 +1,7 @@
+require('vendor/xterm/encoding-indexes.js');
+require('vendor/xterm/encoding.js');
+window.Terminal = require('vendor/xterm/xterm.js');
+require('vendor/xterm/fit.js');
+require('./terminal.js');
+
+$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6
deleted file mode 100644
index 33d2c1e1a17..00000000000
--- a/app/assets/javascripts/terminal/terminal_bundle.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-//= require xterm/encoding-indexes
-//= require xterm/encoding
-//= require xterm/xterm.js
-//= require xterm/fit.js
-//= require ./terminal.js
-
-$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
new file mode 100644
index 00000000000..d48f2404fa5
--- /dev/null
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -0,0 +1,143 @@
+/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
+(function () {
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ 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;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1] ||
+ el
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top + 10;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ 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;
+ simulateEvent(fromEl, 'mousedown', { button: 0 });
+ 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;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
+})();
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
new file mode 100644
index 00000000000..caaf6484a34
--- /dev/null
+++ b/app/assets/javascripts/todos.js
@@ -0,0 +1,146 @@
+/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
+/* global UsersSelect */
+
+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, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
+ $('.todo').off('click', this.goToTodoUrl);
+ }
+
+ bindEvents() {
+ this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
+ this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
+
+ $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
+ $('.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', '');
+ target.classList.add('disabled');
+ $.ajax({
+ type: 'POST',
+ url: target.getAttribute('href'),
+ dataType: 'json',
+ data: {
+ '_method': target.getAttribute('data-method'),
+ },
+ success: (data) => {
+ this.updateRowState(target);
+ return 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);
+ },
+ });
+ }
+
+ 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);
+ }
+ }
+
+ 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 05622916ff8..00000000000
--- a/app/assets/javascripts/todos.js.es6
+++ /dev/null
@@ -1,165 +0,0 @@
-/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
-/* global UsersSelect */
-/* global Turbolinks */
-
-((global) => {
- class Todos {
- constructor({ el } = {}) {
- this.allDoneClicked = this.allDoneClicked.bind(this);
- this.doneClicked = this.doneClicked.bind(this);
- this.el = el || $('.js-todos-options');
- this.perPage = this.el.data('perPage');
- this.clearListeners();
- this.initBtnListeners();
- this.initFilters();
- }
-
- clearListeners() {
- $('.done-todo').off('click');
- $('.js-todos-mark-all').off('click');
- return $('.todo').off('click');
- }
-
- initBtnListeners() {
- $('.done-todo').on('click', this.doneClicked);
- $('.js-todos-mark-all').on('click', this.allDoneClicked);
- return $('.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();
- Turbolinks.visit(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();
- }
- });
- }
-
- doneClicked(e) {
- e.preventDefault();
- e.stopImmediatePropagation();
- const $target = $(e.currentTarget);
- $target.disable();
- return $.ajax({
- type: 'POST',
- url: $target.attr('href'),
- dataType: 'json',
- data: {
- '_method': 'delete'
- },
- success: (data) => {
- this.redirectIfNeeded(data.count);
- this.clearDone($target.closest('li'));
- return this.updateBadges(data);
- }
- });
- }
-
- allDoneClicked(e) {
- e.preventDefault();
- e.stopImmediatePropagation();
- const $target = $(e.currentTarget);
- $target.disable();
- return $.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>');
- return this.updateBadges(data);
- }
- });
- }
-
- clearDone($row) {
- const $ul = $row.closest('ul');
- $row.remove();
- if (!$ul.find('li').length) {
- return $ul.parents('.panel').remove();
- }
- }
-
- updateBadges(data) {
- $(document).trigger('todo:toggle', data.count);
- $('.todos-pending .badge').text(data.count);
- return $('.todos-done .badge').text(data.done_count);
- }
-
- getTotalPages() {
- return this.el.data('totalPages');
- }
-
- getCurrentPage() {
- return this.el.data('currentPage');
- }
-
- getTodosPerPage() {
- return this.el.data('perPage');
- }
-
- redirectIfNeeded(total) {
- const currPages = this.getTotalPages();
- const currPage = this.getCurrentPage();
-
- // Refresh if no remaining Todos
- if (!total) {
- window.location.reload();
- return;
- }
- // Do nothing if no pagination
- if (!currPages) {
- return;
- }
-
- const newPages = Math.ceil(total / this.getTodosPerPage());
- let url = location.href;
-
- if (newPages !== currPages) {
- // Redirect to previous page if there's one available
- if (currPages > 1 && currPage === currPages) {
- const pageParams = {
- page: currPages - 1
- };
- url = gl.utils.mergeUrlParams(pageParams, url);
- }
- return Turbolinks.visit(url);
- }
- }
-
- goToTodoUrl(e) {
- const todoLink = $(this).data('url');
- if (!todoLink) {
- return;
- }
- // Allow Meta-Click or Mouse3-click to open in a new tab
- if (e.metaKey || e.which === 2) {
- e.preventDefault();
- return window.open(todoLink, '_blank');
- } else {
- return Turbolinks.visit(todoLink);
- }
- }
- }
-
- global.Todos = Todos;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index d124ca4f88b..76a821c7a17 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
-/* global Turbolinks */
+
(function() {
this.TreeView = (function() {
function TreeView() {
@@ -15,7 +15,7 @@
e.preventDefault();
return window.open(path, '_blank');
} else {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
@@ -57,7 +57,7 @@
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
@@ -65,4 +65,4 @@
return TreeView;
})();
-}).call(this);
+}).call(window);
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/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 86b459e1866..fd1829efe18 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -24,4 +24,4 @@
return U2FError;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 69d1ff3a39e..17631f2908d 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -95,4 +95,4 @@
return U2FRegister;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 34e88220b12..813d363db00 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -9,4 +9,4 @@
return U2FUtil;
})();
-}).call(this);
+}).call(window);
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 b/app/assets/javascripts/user_tabs.js
new file mode 100644
index 00000000000..465618e3d53
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js
@@ -0,0 +1,158 @@
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
+
+/*
+UserTabs
+
+Handles persisting and restoring the current tab selection and lazily-loading
+content on the Users#show page.
+
+### Example Markup
+
+ <ul class="nav-links">
+ <li class="activity-tab active">
+ <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+ Activity
+ </a>
+ </li>
+ <li class="groups-tab">
+ <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+ Groups
+ </a>
+ </li>
+ <li class="contributed-tab">
+ <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+ Contributed projects
+ </a>
+ </li>
+ <li class="projects-tab">
+ <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+ Personal projects
+ </a>
+ </li>
+ <li class="snippets-tab">
+ <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+ </a>
+ </li>
+ </ul>
+
+ <div class="tab-content">
+ <div class="tab-pane" id="activity">
+ Activity Content
+ </div>
+ <div class="tab-pane" id="groups">
+ Groups Content
+ </div>
+ <div class="tab-pane" id="contributed">
+ Contributed projects content
+ </div>
+ <div class="tab-pane" id="projects">
+ Projects content
+ </div>
+ <div class="tab-pane" id="snippets">
+ Snippets content
+ </div>
+ </div>
+
+ <div class="loading-status">
+ <div class="loading">
+ Loading Animation
+ </div>
+ </div>
+*/
+((global) => {
+ class UserTabs {
+ constructor ({ defaultAction, action, parentEl }) {
+ this.loaded = {};
+ this.defaultAction = defaultAction || 'activity';
+ this.action = action || this.defaultAction;
+ this.$parentEl = $(parentEl) || $(document);
+ this._location = window.location;
+ this.$parentEl.find('.nav-links a')
+ .each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
+ this.actions = Object.keys(this.loaded);
+ this.bindEvents();
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.activateTab(this.action);
+ }
+
+ bindEvents() {
+ return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+ }
+
+ tabShown(event) {
+ const $target = $(event.target);
+ const action = $target.data('action');
+ const source = $target.attr('href');
+ this.setTab(source, action);
+ return this.setCurrentAction(source, action);
+ }
+
+ activateTab(action) {
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
+ .tab('show');
+ }
+
+ setTab(source, action) {
+ if (this.loaded[action]) {
+ return;
+ }
+ if (action === 'activity') {
+ this.loadActivities(source);
+ }
+
+ const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
+ if (loadableActions.indexOf(action) > -1) {
+ return this.loadTab(source, action);
+ }
+ }
+
+ loadTab(source, action) {
+ return $.ajax({
+ beforeSend: () => this.toggleLoading(true),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ url: `${source}.json`,
+ success: (data) => {
+ const tabSelector = `div#${action}`;
+ this.$parentEl.find(tabSelector).html(data.html);
+ this.loaded[action] = true;
+ return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ }
+ });
+ }
+
+ loadActivities(source) {
+ if (this.loaded['activity']) {
+ return;
+ }
+ const $calendarWrap = this.$parentEl.find('.user-calendar');
+ $calendarWrap.load($calendarWrap.data('href'));
+ new gl.Activities();
+ return this.loaded['activity'] = true;
+ }
+
+ toggleLoading(status) {
+ return this.$parentEl.find('.loading-status .loading')
+ .toggle(status);
+ }
+
+ setCurrentAction(source, action) {
+ let new_state = source;
+ new_state = new_state.replace(/\/+$/, '');
+ new_state += this._location.search + this._location.hash;
+ history.replaceState({
+ url: new_state
+ }, document.title, new_state);
+ return new_state;
+ }
+ }
+ global.UserTabs = UserTabs;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
deleted file mode 100644
index 313fb17aee8..00000000000
--- a/app/assets/javascripts/user_tabs.js.es6
+++ /dev/null
@@ -1,159 +0,0 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
-
-/*
-UserTabs
-
-Handles persisting and restoring the current tab selection and lazily-loading
-content on the Users#show page.
-
-### Example Markup
-
- <ul class="nav-links">
- <li class="activity-tab active">
- <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
- Activity
- </a>
- </li>
- <li class="groups-tab">
- <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
- Groups
- </a>
- </li>
- <li class="contributed-tab">
- <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
- Contributed projects
- </a>
- </li>
- <li class="projects-tab">
- <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
- Personal projects
- </a>
- </li>
- <li class="snippets-tab">
- <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
- </a>
- </li>
- </ul>
-
- <div class="tab-content">
- <div class="tab-pane" id="activity">
- Activity Content
- </div>
- <div class="tab-pane" id="groups">
- Groups Content
- </div>
- <div class="tab-pane" id="contributed">
- Contributed projects content
- </div>
- <div class="tab-pane" id="projects">
- Projects content
- </div>
- <div class="tab-pane" id="snippets">
- Snippets content
- </div>
- </div>
-
- <div class="loading-status">
- <div class="loading">
- Loading Animation
- </div>
- </div>
-*/
-((global) => {
- class UserTabs {
- constructor ({ defaultAction, action, parentEl }) {
- this.loaded = {};
- this.defaultAction = defaultAction || 'activity';
- this.action = action || this.defaultAction;
- this.$parentEl = $(parentEl) || $(document);
- this._location = window.location;
- this.$parentEl.find('.nav-links a')
- .each((i, navLink) => {
- this.loaded[$(navLink).attr('data-action')] = false;
- });
- this.actions = Object.keys(this.loaded);
- this.bindEvents();
-
- if (this.action === 'show') {
- this.action = this.defaultAction;
- }
-
- this.activateTab(this.action);
- }
-
- bindEvents() {
- return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
- .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
- }
-
- tabShown(event) {
- const $target = $(event.target);
- const action = $target.data('action');
- const source = $target.attr('href');
- this.setTab(source, action);
- return this.setCurrentAction(source, action);
- }
-
- activateTab(action) {
- return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
- .tab('show');
- }
-
- setTab(source, action) {
- if (this.loaded[action]) {
- return;
- }
- if (action === 'activity') {
- this.loadActivities(source);
- }
-
- const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
- if (loadableActions.indexOf(action) > -1) {
- return this.loadTab(source, action);
- }
- }
-
- loadTab(source, action) {
- return $.ajax({
- beforeSend: () => this.toggleLoading(true),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- type: 'GET',
- url: `${source}.json`,
- success: (data) => {
- const tabSelector = `div#${action}`;
- this.$parentEl.find(tabSelector).html(data.html);
- this.loaded[action] = true;
- return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
- }
- });
- }
-
- loadActivities(source) {
- if (this.loaded['activity']) {
- return;
- }
- const $calendarWrap = this.$parentEl.find('.user-calendar');
- $calendarWrap.load($calendarWrap.data('href'));
- new gl.Activities();
- return this.loaded['activity'] = true;
- }
-
- toggleLoading(status) {
- return this.$parentEl.find('.loading-status .loading')
- .toggle(status);
- }
-
- setCurrentAction(source, action) {
- let new_state = source;
- new_state = new_state.replace(/\/+$/, '');
- new_state += this._location.search + this._location.hash;
- history.replaceState({
- turbolinks: true,
- url: new_state
- }, document.title, new_state);
- return new_state;
- }
- }
- global.UserTabs = UserTabs;
-})(window.gl || (window.gl = {}));
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 e7280d643d3..754d448564f 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,6 +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 */
-/* global dateFormat */
+
+import d3 from 'd3';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -33,7 +33,7 @@
date.setDate(date.getDate() + i);
var day = date.getDay();
- var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
+ var count = timestamps[date.format('yyyy-mm-dd')];
// Create a new group array if this is the first day of the week
// or if is first object
@@ -122,7 +122,7 @@
if (stamp.count > 0) {
contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
}
- dateText = dateFormat(date, 'mmm d, yyyy');
+ dateText = date.format('mmm d, yyyy');
return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
};
})(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
@@ -222,4 +222,4 @@
return Calendar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index f50802bdf2e..580e2d84be5 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,7 +1 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+require('./calendar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 77d2764cdf0..27af859f7d8 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -8,7 +8,8 @@
slice = [].slice;
this.UsersSelect = (function() {
- function UsersSelect(currentUser) {
+ function UsersSelect(currentUser, els) {
+ var $els;
this.users = bind(this.users, this);
this.user = bind(this.user, this);
this.usersPath = "/autocomplete/users.json";
@@ -20,7 +21,14 @@
this.currentUser = JSON.parse(currentUser);
}
}
- $('.js-user-search').each((function(_this) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-user-search');
+ }
+
+ $els.each((function(_this) {
return function(i, dropdown) {
var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
@@ -52,6 +60,15 @@
});
};
+ $('.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();
@@ -191,9 +208,16 @@
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 ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ 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();
@@ -224,11 +248,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>";
@@ -238,7 +267,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 === '') {
@@ -422,4 +451,4 @@
return UsersSelect;
})();
-}).call(this);
+}).call(window);
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_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
deleted file mode 100644
index 62a22e39a3b..00000000000
--- a/app/assets/javascripts/vue_common_component/commit.js.es6
+++ /dev/null
@@ -1,163 +0,0 @@
-/*= require vue */
-/* global 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_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6
deleted file mode 100644
index 605824fa939..00000000000
--- a/app/assets/javascripts/vue_pagination/index.js.es6
+++ /dev/null
@@ -1,148 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-((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: {
-
- /**
- 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) {
- Turbolinks.visit(`?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) {
- let apiScope = gl.utils.getParameterByName('scope');
-
- if (!apiScope) apiScope = 'all';
-
- 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_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
new file mode 100644
index 00000000000..a90bd1518e9
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -0,0 +1,29 @@
+/* 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');
+
+ return {
+ scope: project.dataset.url,
+ store: new gl.PipelineStore(),
+ };
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope="scope"
+ :store="store">
+ </vue-pipelines>
+ `,
+}));
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 edd01f17a97..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ /dev/null
@@ -1,42 +0,0 @@
-/* global Vue, VueResource, gl */
-/*= require vue_common_component/commit */
-/*= require vue_pagination/index */
-/*= require vue-resource
-/*= require boards/vue_resource_interceptor */
-/*= require ./status.js.es6 */
-/*= require ./store.js.es6 */
-/*= require ./pipeline_url.js.es6 */
-/*= require ./stage.js.es6 */
-/*= require ./stages.js.es6 */
-/*= require ./pipeline_actions.js.es6 */
-/*= require ./time_ago.js.es6 */
-/*= require ./pipelines.js.es6 */
-
-(() => {
- const project = document.querySelector('.pipelines');
- const entry = document.querySelector('.vue-pipelines-index');
- const svgs = document.querySelector('.pipeline-svgs');
-
- Vue.use(VueResource);
-
- if (!entry) return null;
- return new Vue({
- el: entry,
- data: {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
- svgs: svgs.dataset,
- },
- 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 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
new file mode 100644
index 00000000000..891f1f17fb3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
@@ -0,0 +1,119 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign, no-alert */
+const playIconSvg = require('icons/_icon_play.svg');
+
+((gl) => {
+ gl.VuePipelineActions = Vue.extend({
+ props: ['pipeline'],
+ 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`;
+ },
+
+ /**
+ * Shows a dialog when the user clicks in the cancel button.
+ * We need to prevent the default behavior and stop propagation because the
+ * link relies on UJS.
+ *
+ * @param {Event} event
+ */
+ confirmAction(event) {
+ if (!confirm('Are you sure you want to cancel this pipeline?')) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+ },
+
+ data() {
+ return { playIconSvg };
+ },
+
+ template: `
+ <td class="pipeline-actions">
+ <div class="pull-right">
+ <div class="btn-group">
+ <div class="btn-group" v-if="actions">
+ <button
+ 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="playIconSvg" 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="playIconSvg" aria-hidden="true"></span>
+ <span>{{action.name}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="btn-group" v-if="artifacts">
+ <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 pipeline.details.artifacts'>
+ <a
+ rel="nofollow"
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>{{download(artifact.name)}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="btn-group" v-if="pipeline.flags.retryable">
+ <a
+ class="btn btn-default btn-retry 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>
+ </div>
+ <div class="btn-group" v-if="pipeline.flags.cancelable">
+ <a
+ 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>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
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 b195b0ef3ba..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ /dev/null
@@ -1,108 +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 build"
- data-placement="top"
- data-toggle="dropdown"
- aria-label="Manual build"
- >
- <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"
- data-toggle="dropdown"
- 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"
- :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
index ae5649f0519..ae5649f0519 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
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..601ef41e917
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -0,0 +1,87 @@
+/* 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: {},
+ pageRequest: false,
+ };
+ },
+ props: ['scope', 'store'],
+ 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: {
+ /**
+ * 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;
+ },
+ },
+ 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'/>
+ </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/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
deleted file mode 100644
index b2ed05503c9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ /dev/null
@@ -1,131 +0,0 @@
-/* global Vue, Turbolinks, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelines = Vue.extend({
- components: {
- runningPipeline: gl.VueRunningPipeline,
- pipelineActions: gl.VuePipelineActions,
- stages: gl.VueStages,
- commit: gl.CommitComponent,
- pipelineUrl: gl.VuePipelineUrl,
- pipelineHead: gl.VuePipelineHead,
- glPagination: gl.VueGlPagination,
- statusScope: gl.VueStatusScope,
- timeAgo: gl.VueTimeAgo,
- },
- 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('p');
- 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);
- },
- methods: {
- change(pagenum, apiScope) {
- Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
- },
- author(pipeline) {
- if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
- if (pipeline.commit.author) return pipeline.commit.author;
- return {
- avatar_url: pipeline.commit.author_gravatar_url,
- web_url: `mailto:${pipeline.commit.author_email}`,
- username: pipeline.commit.author_name,
- };
- },
- ref(pipeline) {
- const { ref } = pipeline;
- return { name: ref.name, tag: ref.tag, ref_url: ref.path };
- },
- commitTitle(pipeline) {
- return pipeline.commit ? pipeline.commit.title : '';
- },
- commitSha(pipeline) {
- return pipeline.commit ? pipeline.commit.short_id : '';
- },
- commitUrl(pipeline) {
- return pipeline.commit ? pipeline.commit.commit_path : '';
- },
- match(string) {
- return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
- },
- },
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
- <i class="fa fa-spinner fa-spin"></i>
- </div>
- <div class="table-holder" v-if='pipelines.length'>
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="pipeline-status">Status</th>
- <th class="pipeline-info">Pipeline</th>
- <th class="pipeline-commit">Commit</th>
- <th class="pipeline-stages">Stages</th>
- <th class="pipeline-date"></th>
- <th class="pipeline-actions hidden-xs"></th>
- </tr>
- </thead>
- <tbody>
- <tr class="commit" v-for='pipeline in pipelines'>
- <status-scope
- :pipeline='pipeline'
- :match='match'
- :svgs='svgs'
- >
- </status-scope>
- <pipeline-url :pipeline='pipeline'></pipeline-url>
- <td>
- <commit
- :commit-icon-svg='svgs.commitIconSvg'
- :author='author(pipeline)'
- :tag="pipeline.ref.tag"
- :title='commitTitle(pipeline)'
- :commit-ref='ref(pipeline)'
- :short-sha='commitSha(pipeline)'
- :commit-url='commitUrl(pipeline)'
- >
- </commit>
- </td>
- <stages
- :pipeline='pipeline'
- :svgs='svgs'
- :match='match'
- >
- </stages>
- <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
- <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
- </tr>
- </tbody>
- </table>
- </div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
- </div>
- <gl-pagination
- v-if='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/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
new file mode 100644
index 00000000000..ae4f0b4a53b
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js
@@ -0,0 +1,119 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+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';
+
+((gl) => {
+ gl.VueStage = Vue.extend({
+ 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 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;
+ });
+ },
+
+ /**
+ * 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).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (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>
+ `,
+ });
+})(window.gl || (window.gl = {}));
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 496df9aaced..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: DOMStringMap,
- 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/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
deleted file mode 100644
index cb176b3f0c6..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6
+++ /dev/null
@@ -1,21 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueStages = Vue.extend({
- components: {
- 'vue-stage': gl.VueStage,
- },
- props: ['pipeline', 'svgs', 'match'],
- template: `
- <td class="stage-cell">
- <div
- class="stage-container dropdown js-mini-pipeline-graph"
- v-for='stage in pipeline.details.stages'
- >
- <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
new file mode 100644
index 00000000000..8d9f83ac113
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/status.js
@@ -0,0 +1,64 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+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';
+
+((gl) => {
+ gl.VueStatusScope = Vue.extend({
+ props: [
+ 'pipeline',
+ ],
+
+ 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() {
+ const cssObject = { 'ci-status': true };
+ cssObject[`ci-${this.pipeline.details.status.group}`] = true;
+ return cssObject;
+ },
+
+ 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>
+ `,
+ });
+})(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 b/app/assets/javascripts/vue_pipelines_index/store.js
new file mode 100644
index 00000000000..909007267b9
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/store.js
@@ -0,0 +1,31 @@
+/* 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/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
deleted file mode 100644
index 9e19b1564dc..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ /dev/null
@@ -1,65 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign, no-underscore-dangle */
-/*= require vue_realtime_listener/index.js */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
-
- const paginationInfo = {
- perPage: +normalized['X-PER-PAGE'],
- page: +normalized['X-PAGE'],
- total: +normalized['X-TOTAL'],
- totalPages: +normalized['X-TOTAL-PAGES'],
- nextPage: +normalized['X-NEXT-PAGE'],
- previousPage: +normalized['X-PREV-PAGE'],
- };
-
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- const updatePipelineNums = (count) => {
- const { all } = count;
- const running = count.running_or_pending;
- document.querySelector('.js-totalbuilds-count').innerHTML = all;
- document.querySelector('.js-running-count').innerHTML = running;
- };
-
- const goFetch = () =>
- 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);
-
- updatePipelineNums(this.count);
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('Something went wrong on our end.');
- });
-
- goFetch();
-
- const startTimeLoops = () => {
- this.timeLoopInterval = setInterval(() => {
- this.$children
- .filter(e => e.$options._componentTag === 'time-ago')
- .forEach(e => e.changeTime());
- }, 10000);
- };
-
- startTimeLoops();
-
- const removeIntervals = () => clearInterval(this.timeLoopInterval);
- const startIntervals = () => startTimeLoops();
-
- gl.VueRealtimeListener(removeIntervals, startIntervals);
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
new file mode 100644
index 00000000000..a383570857d
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js
@@ -0,0 +1,78 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+window.Vue = require('vue');
+require('../lib/utils/datetime_utility');
+
+const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
+
+((gl) => {
+ gl.VueTimeAgo = Vue.extend({
+ 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>
+ `,
+ });
+})(window.gl || (window.gl = {}));
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 655110feba1..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ /dev/null
@@ -1,73 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((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 b/app/assets/javascripts/vue_realtime_listener/index.js
new file mode 100644
index 00000000000..30f6680a673
--- /dev/null
+++ b/app/assets/javascripts/vue_realtime_listener/index.js
@@ -0,0 +1,29 @@
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
+ const removeAll = () => {
+ removeIntervals();
+ window.removeEventListener('beforeunload', removeIntervals);
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ document.removeEventListener('beforeunload', removeAll);
+ };
+
+ window.addEventListener('beforeunload', removeIntervals);
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ document.addEventListener('beforeunload', removeAll);
+
+ // add removeAll methods to stack
+ const stack = gl.VueRealtimeListener.reset;
+ gl.VueRealtimeListener.reset = () => {
+ gl.VueRealtimeListener.reset = stack;
+ removeAll();
+ stack();
+ };
+ };
+
+ // remove all event listeners and intervals
+ gl.VueRealtimeListener.reset = () => undefined; // noop
+})(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.es6
deleted file mode 100644
index 23cac1466d2..00000000000
--- a/app/assets/javascripts/vue_realtime_listener/index.js.es6
+++ /dev/null
@@ -1,18 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
- const removeAll = () => {
- removeIntervals();
- window.removeEventListener('beforeunload', removeIntervals);
- window.removeEventListener('focus', startIntervals);
- window.removeEventListener('blur', removeIntervals);
- document.removeEventListener('page:fetch', removeAll);
- };
-
- window.addEventListener('beforeunload', removeIntervals);
- window.addEventListener('focus', startIntervals);
- window.addEventListener('blur', removeIntervals);
- document.addEventListener('page:fetch', removeAll);
- };
-})(window.gl || (window.gl = {}));
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..4381487b79e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -0,0 +1,164 @@
+/* global Vue */
+window.Vue = require('vue');
+const commitIconSvg = require('icons/_icon_commit.svg');
+
+(() => {
+ 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: () => ({}),
+ },
+ },
+
+ 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/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
new file mode 100644
index 00000000000..0d8f85db965
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -0,0 +1,52 @@
+/* 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: () => ([]),
+ },
+
+ },
+
+ 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"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"></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..e5e88186a85
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -0,0 +1,199 @@
+/* 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: () => ({}),
+ },
+
+ },
+
+ 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;
+ },
+ },
+
+ 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"/>
+
+ <pipeline-actions :pipeline="pipeline" />
+ </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..8943b850a72
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -0,0 +1,147 @@
+/* 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
+
+ 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++) {
+ 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..4157fefddc9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -0,0 +1,19 @@
+/* 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) => {
+ 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 b/app/assets/javascripts/wikis.js
new file mode 100644
index 00000000000..75fd1394a03
--- /dev/null
+++ b/app/assets/javascripts/wikis.js
@@ -0,0 +1,69 @@
+/* eslint-disable no-param-reassign */
+/* global Breakpoints */
+
+require('./breakpoints');
+require('vendor/jquery.nicescroll');
+
+((global) => {
+ class Wikis {
+ constructor() {
+ this.bp = Breakpoints.get();
+ this.sidebarEl = document.querySelector('.js-wiki-sidebar');
+ this.sidebarExpanded = false;
+ $(this.sidebarEl).niceScroll();
+
+ const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
+ for (let i = 0; i < sidebarToggles.length; i += 1) {
+ sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
+ }
+
+ this.newWikiForm = document.querySelector('form.new-wiki-page');
+ if (this.newWikiForm) {
+ this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ }
+
+ window.addEventListener('resize', () => this.renderSidebar());
+ this.renderSidebar();
+ }
+
+ handleNewWikiSubmit(e) {
+ if (!this.newWikiForm) return;
+
+ const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
+ const slug = gl.text.slugify(slugInput.value);
+
+ if (slug.length > 0) {
+ const wikisPath = slugInput.getAttribute('data-wikis-path');
+ window.location.href = `${wikisPath}/${slug}`;
+ e.preventDefault();
+ }
+ }
+
+ handleToggleSidebar(e) {
+ e.preventDefault();
+ this.sidebarExpanded = !this.sidebarExpanded;
+ this.renderSidebar();
+ }
+
+ sidebarCanCollapse() {
+ const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ }
+
+ renderSidebar() {
+ if (!this.sidebarEl) return;
+ const { classList } = this.sidebarEl;
+ if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
+ if (!classList.contains('right-sidebar-expanded')) {
+ classList.remove('right-sidebar-collapsed');
+ classList.add('right-sidebar-expanded');
+ }
+ } else if (classList.contains('right-sidebar-expanded')) {
+ classList.add('right-sidebar-collapsed');
+ classList.remove('right-sidebar-expanded');
+ }
+ }
+ }
+
+ global.Wikis = Wikis;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
deleted file mode 100644
index ecff5fd5bf4..00000000000
--- a/app/assets/javascripts/wikis.js.es6
+++ /dev/null
@@ -1,73 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Breakpoints */
-
-/*= require latinise */
-/*= require breakpoints */
-/*= require jquery.nicescroll */
-
-((global) => {
- const dasherize = str => str.replace(/[_\s]+/g, '-');
- const slugify = str => dasherize(str.trim().toLowerCase().latinise());
-
- class Wikis {
- constructor() {
- this.bp = Breakpoints.get();
- this.sidebarEl = document.querySelector('.js-wiki-sidebar');
- this.sidebarExpanded = false;
- $(this.sidebarEl).niceScroll();
-
- const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
- for (let i = 0; i < sidebarToggles.length; i += 1) {
- sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
- }
-
- this.newWikiForm = document.querySelector('form.new-wiki-page');
- if (this.newWikiForm) {
- this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
- }
-
- window.addEventListener('resize', () => this.renderSidebar());
- this.renderSidebar();
- }
-
- handleNewWikiSubmit(e) {
- if (!this.newWikiForm) return;
-
- const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = slugify(slugInput.value);
-
- if (slug.length > 0) {
- const wikisPath = slugInput.getAttribute('data-wikis-path');
- window.location.href = `${wikisPath}/${slug}`;
- e.preventDefault();
- }
- }
-
- handleToggleSidebar(e) {
- e.preventDefault();
- this.sidebarExpanded = !this.sidebarExpanded;
- this.renderSidebar();
- }
-
- sidebarCanCollapse() {
- const bootstrapBreakpoint = this.bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
- }
-
- renderSidebar() {
- if (!this.sidebarEl) return;
- const { classList } = this.sidebarEl;
- if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
- if (!classList.contains('right-sidebar-expanded')) {
- classList.remove('right-sidebar-collapsed');
- classList.add('right-sidebar-expanded');
- }
- } else if (classList.contains('right-sidebar-expanded')) {
- classList.add('right-sidebar-collapsed');
- classList.remove('right-sidebar-expanded');
- }
- }
- }
-
- global.Wikis = Wikis;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index a8b7be7ad06..ce626cf7b46 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -6,11 +6,11 @@
//
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
-//
-/*= require jquery.scrollTo */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
+
+require('vendor/jquery.scrollTo');
+window.Dropzone = require('dropzone');
+require('mousetrap');
+require('mousetrap/plugins/pause/mousetrap-pause');
//
// ### Events
@@ -94,4 +94,4 @@
return ZenMode;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 8b93665d085..83a8eeaafde 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,8 +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/datepicker
- *= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
@@ -19,6 +17,8 @@
* directory.
*/
+@import "../../../node_modules/pikaday/scss/pikaday";
+
/*
* GitLab UI framework
*/
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 3cf49f4ff1b..5bb7e8caec1 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -19,7 +19,6 @@
@import "framework/flash.scss";
@import "framework/forms.scss";
@import "framework/gfm.scss";
-@import "framework/gitlab-theme.scss";
@import "framework/header.scss";
@import "framework/highlight.scss";
@import "framework/issue_box.scss";
@@ -31,7 +30,6 @@
@import "framework/modal.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
-@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@@ -46,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/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 8d38fc78a19..90935b9616b 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -71,6 +71,27 @@
transition: $unfoldedTransitions;
}
+@mixin disableAllAnimation {
+ /*CSS transitions*/
+ -o-transition-property: none !important;
+ -moz-transition-property: none !important;
+ -ms-transition-property: none !important;
+ -webkit-transition-property: none !important;
+ transition-property: none !important;
+ /*CSS transforms*/
+ -o-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ /*CSS animations*/
+ -webkit-animation: none !important;
+ -moz-animation: none !important;
+ -o-animation: none !important;
+ -ms-animation: none !important;
+ animation: none !important;
+}
+
@function unfoldTransition ($transition) {
// Default values
$property: all;
@@ -95,7 +116,7 @@
}
.btn,
-.side-nav-toggle {
+.global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow);
}
@@ -107,8 +128,7 @@
.note-action-button .link-highlight,
.toolbar-btn,
-.dropdown-toggle-caret,
-.fa:not(.fa-bell) {
+.dropdown-toggle-caret {
@include transition(color);
}
@@ -116,11 +136,12 @@ a {
@include transition(background-color, color, border);
}
-.tree-table td,
-.well-list > li {
- @include transition(background-color, border-color);
-}
-
.stage-nav-item {
@include transition(background-color, box-shadow);
}
+
+.dropdown-menu a,
+.dropdown-menu button,
+.dropdown-menu-nav a {
+ transition: none;
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 1d59700543c..3f5b78ed445 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -28,6 +28,8 @@
.avatar {
@extend .avatar-circle;
+ @include transition-property(none);
+
width: 40px;
height: 40px;
padding: 0;
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 49907417e26..f363affa46c 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;
@@ -97,6 +99,8 @@
padding: 5px 6px;
outline: 0;
+ line-height: 1;
+
&.disabled {
cursor: default;
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 1d2d1bfc0d7..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) {
@@ -9,6 +10,8 @@
}
.user-calendar-activities {
+ direction: ltr;
+
.str-truncated {
max-width: 70%;
}
@@ -43,3 +46,56 @@
float: right;
font-size: 12px;
}
+
+.pika-single.gitlab-theme {
+ .pika-label {
+ color: $gl-text-color-secondary;
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+ th {
+ padding: 2px 0;
+ color: $note-disabled-comment-color;
+ font-weight: normal;
+ text-transform: lowercase;
+ border-top: 1px solid $calendar-border-color;
+ }
+
+ abbr {
+ cursor: default;
+ }
+
+ td {
+ border: 1px solid $calendar-border-color;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+ }
+
+ .pika-day {
+ border-radius: 0;
+ background-color: $white-light;
+ text-align: center;
+ }
+
+ .is-today {
+ .pika-day {
+ color: inherit;
+ font-weight: normal;
+ }
+ }
+
+ .is-selected .pika-day,
+ .pika-day:hover,
+ .is-today .pika-day:hover {
+ background: $gl-primary;
+ color: $white-light;
+ box-shadow: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0ce94a26a7f..a4b38723bbd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -253,6 +253,8 @@ li.note {
.progress {
margin-bottom: 0;
margin-top: 4px;
+ box-shadow: none;
+ background-color: $border-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6bfb9a6d1cb..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;
@@ -125,7 +126,6 @@
top: 100%;
left: 0;
z-index: 9;
- max-width: 280px;
min-width: 240px;
margin-top: 2px;
margin-bottom: 0;
@@ -137,6 +137,10 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ .filtered-search-input-container & {
+ max-width: 280px;
+ }
+
&.is-loading {
.dropdown-content {
display: none;
@@ -155,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;
}
@@ -177,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;
@@ -189,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 {
@@ -210,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 {
@@ -225,6 +235,17 @@
padding: 5px 8px;
color: $gl-text-color-secondary;
}
+
+ .badge {
+ position: absolute;
+ right: 8px;
+ top: 5px;
+ }
+}
+
+.dropdown-menu-drop-up {
+ top: auto;
+ bottom: 100%;
}
.dropdown-menu-large {
@@ -305,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;
@@ -334,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;
@@ -364,7 +388,7 @@
right: 5px;
width: 20px;
height: 20px;
- top: -3px;
+ top: -1px;
}
.dropdown-menu-back {
@@ -497,119 +521,16 @@
max-height: 230px;
}
- .ui-widget {
- table {
- margin: 0;
- }
-
- &.ui-datepicker-inline {
- padding: 0 10px;
- border: 0;
- width: 100%;
- }
-
- .ui-datepicker-header {
- padding: 0 8px 10px;
- border: 0;
-
- .ui-icon {
- background: none;
- font-size: 20px;
- text-indent: 0;
-
- &::before {
- display: block;
- position: relative;
- top: -2px;
- color: $dropdown-title-btn-color;
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- }
- }
-
- .ui-datepicker-calendar {
- .ui-state-hover,
- .ui-state-active {
- color: $white-light;
- border: 0;
- }
- }
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 0;
- height: 15px;
- cursor: pointer;
-
- &:hover {
- background-color: transparent;
- border: 0;
-
- .ui-icon::before {
- color: $md-link-color;
- }
- }
- }
-
- .ui-datepicker-prev {
- left: 0;
-
- .ui-icon::before {
- content: '\f104';
- text-align: left;
- }
- }
-
- .ui-datepicker-next {
- right: 0;
-
- .ui-icon::before {
- content: '\f105';
- text-align: right;
- }
- }
-
- td {
- padding: 0;
- border: 1px solid $calendar-border-color;
-
- &:first-child {
- border-left: 0;
- }
-
- &:last-child {
- border-right: 0;
- }
-
- a {
- line-height: 17px;
- border: 0;
- border-radius: 0;
- }
- }
-
- .ui-datepicker-title {
- color: $gl-text-color;
- font-size: 14px;
- line-height: 1;
- font-weight: normal;
- }
- }
-
- th {
- padding: 2px 0;
- color: $note-disabled-comment-color;
- font-weight: normal;
- text-transform: lowercase;
- border-top: 1px solid $calendar-border-color;
+ .pika-single {
+ position: relative!important;
+ top: 0!important;
+ border: 0;
+ box-shadow: none;
}
- .ui-datepicker-unselectable {
- background-color: $gray-light;
+ .pika-lendar {
+ margin-top: -5px;
+ margin-bottom: 0;
}
}
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..0a8bc95590e 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,1809 +1,6 @@
-.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-size: 1.5em;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c51912b4ac4..ffece53a093 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -231,3 +231,47 @@ span.idiff {
}
}
}
+
+.file-title-flex-parent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: 3px 3px 0 0;
+
+ .file-header-content {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 30px;
+ position: relative;
+ }
+
+ .btn-clipboard {
+ position: absolute;
+ right: 0;
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ small {
+ margin: 0 10px 0 0;
+ }
+
+ .file-actions {
+ white-space: nowrap;
+
+ .btn {
+ padding: 0 10px;
+ 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..8f2150066c7 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,104 @@
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 {
+ flex: 1;
+ -webkit-flex: 1;
+ }
+
+ .filtered-search-token + .input-token:not(:last-child) {
+ max-width: 200px;
+ }
+}
+
+.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: scroll;
+ white-space: nowrap;
+ width: 100%;
}
.filtered-search-input-container {
@@ -33,14 +154,41 @@
display: flex;
position: relative;
width: 100%;
+ border: 1px solid $border-color;
+ background-color: $white-light;
+ max-width: 87%;
+
+ @media (max-width: $screen-xs-min) {
+ -webkit-flex: 1 1 100%;
+ flex: 1 1 100%;
+ margin-bottom: 10px;
+
+ .dropdown-menu {
+ width: auto;
+ left: 0;
+ right: 0;
+ max-width: none;
+ min-width: 100%;
+ }
+ }
.form-control {
- padding-left: 25px;
+ position: relative;
+ min-width: 200px;
+ padding-left: 0;
padding-right: 25px;
+ border-color: transparent;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
+
+ &:focus,
+ &:hover {
+ outline: none;
+ border-color: transparent;
+ box-shadow: none;
+ }
}
.fa-filter {
@@ -57,12 +205,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;
@@ -79,6 +228,39 @@
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;
+ }
+
+ .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/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
deleted file mode 100644
index d6566dc4ec9..00000000000
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Styles the GitLab application with a specific color theme
- *
- * $color-light -
- * $color -
- * $color-darker -
- * $color-dark -
- */
-@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
- .page-with-sidebar {
- .toggle-nav-collapse,
- .pin-nav-btn {
- color: $color-light;
-
- &:hover {
- color: $white-light;
- }
- }
-
- .sidebar-wrapper {
- background: $color-darker;
- }
-
- .sidebar-action-buttons {
- color: $color-light;
- background-color: lighten($color-darker, 5%);
- }
-
- .nav-sidebar {
- li {
- a {
- color: $color-light;
-
- &:hover,
- &:focus,
- &:active {
- background: $color-dark;
- }
-
- i {
- color: $color-light;
- }
-
- path,
- polygon {
- fill: $color-light;
- }
-
- .count {
- color: $color-light;
- background: $color-dark;
- }
-
- svg {
- position: relative;
- top: 3px;
- }
- }
-
- &.separate-item {
- border-top: 1px solid $color;
- }
-
- &.active a {
- color: $white-light;
- background: $color-dark;
-
- &.no-highlight {
- border: none;
- }
-
- i {
- color: $white-light;
- }
-
- path,
- polygon {
- fill: $white-light;
- }
- }
- }
-
- .about-gitlab {
- color: $color-light;
- }
- }
- }
-}
-
-$theme-charcoal-light: #b9bbbe;
-$theme-charcoal: #485157;
-$theme-charcoal-dark: #3d454d;
-$theme-charcoal-darker: #383f45;
-
-$theme-blue-light: #becde9;
-$theme-blue: #2980b9;
-$theme-blue-dark: #1970a9;
-$theme-blue-darker: #096099;
-
-$theme-graphite-light: #ccc;
-$theme-graphite: #777;
-$theme-graphite-dark: #666;
-$theme-graphite-darker: #555;
-
-$theme-black-light: #979797;
-$theme-black: #373737;
-$theme-black-dark: #272727;
-$theme-black-darker: #222;
-
-$theme-green-light: #adc;
-$theme-green: #019875;
-$theme-green-dark: #018865;
-$theme-green-darker: #017855;
-
-$theme-violet-light: #98c;
-$theme-violet: #548;
-$theme-violet-dark: #436;
-$theme-violet-darker: #325;
-
-body {
- &.ui_blue {
- @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
- }
-
- &.ui_charcoal {
- @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
- }
-
- &.ui_graphite {
- @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
- }
-
- &.ui_black {
- @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
- }
-
- &.ui_green {
- @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
- }
-
- &.ui_violet {
- @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
- }
-}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2a01bc4d44d..5d1aba4e529 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -100,23 +100,40 @@ header {
}
}
}
+ }
- .side-nav-toggle {
- position: absolute;
- left: -10px;
- margin: 7px 0;
- font-size: 18px;
- padding: 6px 10px;
- border: none;
- background-color: $gray-light;
+ .global-dropdown {
+ position: absolute;
+ left: -10px;
- &:hover {
- background-color: $white-normal;
- color: $gl-header-nav-hover-color;
+ .badge {
+ font-size: 11px;
+ }
+
+ li {
+ &.active a {
+ font-weight: bold;
}
}
}
+ .global-dropdown-toggle {
+ margin: 7px 0;
+ font-size: 18px;
+ padding: 6px 10px;
+ border: none;
+ background-color: $gray-light;
+
+ &:hover {
+ background-color: $white-normal;
+ }
+
+ &:focus {
+ outline: none;
+ background-color: $white-normal;
+ }
+ }
+
.header-content {
position: relative;
height: $header-height;
@@ -131,34 +148,20 @@ header {
}
.header-logo {
- position: absolute;
- left: 50%;
- top: 7px;
+ display: inline-block;
+ margin: 0 7px 0 2px;
+ position: relative;
+ top: 10px;
transition-duration: .3s;
- z-index: 999;
-
- #logo {
- position: relative;
- left: -50%;
- }
svg,
img {
- height: 36px;
+ height: 28px;
}
&:hover {
cursor: pointer;
}
-
- @media (max-width: $screen-xs-max) {
- right: 20px;
- left: auto;
-
- #logo {
- left: auto;
- }
- }
}
.title {
@@ -166,7 +169,6 @@ header {
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -176,14 +178,18 @@ header {
vertical-align: top;
white-space: nowrap;
- @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;
@@ -222,6 +228,10 @@ header {
float: right;
border-top: none;
+ @media (min-width: $screen-md-min) {
+ padding: 0;
+ }
+
@media (max-width: $screen-xs-max) {
float: none;
}
@@ -250,29 +260,39 @@ 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 {
.dropdown-menu-nav {
- width: 140px;
+ min-width: 140px;
margin-top: -5px;
}
}
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 18f2f316f02..300ba4f2de6 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,53 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
- &.ui-datepicker,
- &.ui-datepicker-inline {
- border: 1px solid $jq-ui-border;
- padding: 10px;
- width: 270px;
-
- .ui-datepicker-header {
- background: $white-light;
- border-color: $jq-ui-border;
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 4px;
- }
-
- .ui-datepicker-prev {
- left: 2px;
- }
-
- .ui-datepicker-next {
- right: 2px;
- }
-
- .ui-state-hover {
- background: transparent;
- border: 0;
- cursor: pointer;
- }
- }
-
- .ui-datepicker-calendar td a {
- padding: 5px;
- text-align: center;
- }
- }
-
- &.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;
@@ -59,25 +12,4 @@
border: 0;
background: transparent;
}
-
- .ui-datepicker-calendar {
- .ui-state-active,
- .ui-state-hover,
- .ui-state-focus {
- border: 1px solid $gl-primary;
- background: $gl-primary;
- color: $white-light;
- }
- }
-}
-
-.ui-sortable-handle {
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
}
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 426596027de..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;
}
@@ -307,3 +259,7 @@ ul.controls {
}
}
}
+
+ul.indent-list {
+ padding: 10px 0 0 30px;
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5bff694658c..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;
@@ -159,6 +162,11 @@
.cur {
.avatar {
border: 1px solid $white-light;
+ @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 fd081c2d7e1..ea45aaa0253 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -283,10 +283,7 @@
}
.layout-nav {
- position: fixed;
- top: $header-height;
width: 100%;
- z-index: 11;
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
@@ -297,7 +294,7 @@
.nav-control {
@media (max-width: $screen-sm-max) {
- margin-right: 75px;
+ margin-right: 2px;
}
}
}
@@ -419,15 +416,20 @@
}
.page-with-layout-nav {
- margin-top: $header-height + 2;
-
.right-sidebar {
top: ($header-height * 2) + 2;
}
+
+ .build-sidebar {
+ top: ($header-height * 3) + 3;
+
+ &.affix {
+ top: 0;
+ }
+ }
}
.activities {
-
.nav-block {
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b37c1d0d670..c3ec9db0f07 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -6,8 +6,22 @@
.pagination {
padding: 0;
+
+ a {
+ cursor: pointer;
+ }
+
+ .separator,
+ .separator:hover {
+ a {
+ cursor: default;
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ }
+ }
}
+
.gap,
.gap:hover {
background-color: $gray-light;
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/progress.scss b/app/assets/stylesheets/framework/progress.scss
deleted file mode 100644
index e9800bd24b5..00000000000
--- a/app/assets/stylesheets/framework/progress.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-html.turbolinks-progress-bar::before {
- background-color: $progress-color!important;
- height: 2px!important;
- box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
-}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index f0b03710c79..40e93032f59 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,36 +1,3 @@
-.page-with-sidebar {
- padding: $header-height 0 25px;
- transition: padding $sidebar-transition-duration;
-
- &.page-sidebar-pinned {
- .sidebar-wrapper {
- box-shadow: none;
- }
- }
-
- .sidebar-wrapper {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- height: 100%;
- width: 0;
- overflow: hidden;
- transition: width $sidebar-transition-duration;
- box-shadow: 2px 0 16px 0 $black-transparent;
- }
-}
-
-.sidebar-wrapper {
- z-index: 1000;
- background: $gray-light;
-
- .nicescroll-rails-hr {
- // TODO: Figure out why nicescroll doesn't hide horizontal bar
- display: none!important;
- }
-}
-
.content-wrapper {
width: 100%;
transition: padding $sidebar-transition-duration;
@@ -47,105 +14,6 @@
}
}
-.nav-sidebar {
- position: absolute;
- top: 50px;
- bottom: 0;
- width: $sidebar_width;
- overflow-y: auto;
- overflow-x: hidden;
-
- &.navbar-collapse {
- padding: 0 !important;
- }
-
- li {
- &.separate-item {
- padding-top: 10px;
- margin-top: 10px;
- }
-
- .icon-container {
- width: 34px;
- display: inline-block;
- text-align: center;
- }
-
- a {
- padding: 7px $gl-sidebar-padding;
- font-size: $gl-font-size;
- line-height: 24px;
- display: block;
- text-decoration: none;
- font-weight: normal;
-
- &:hover,
- &:active,
- &:focus {
- text-decoration: none;
- }
-
- i {
- font-size: 16px;
- }
-
- i,
- svg {
- margin-right: 13px;
- }
- }
- }
-
- .count {
- float: right;
- padding: 0 8px;
- border-radius: 6px;
- }
-
- .about-gitlab {
- padding: 7px $gl-sidebar-padding;
- font-size: $gl-font-size;
- line-height: 24px;
- display: block;
- text-decoration: none;
- font-weight: normal;
- position: absolute;
- bottom: 10px;
- }
-}
-
-.sidebar-action-buttons {
- width: $sidebar_width;
- position: absolute;
- top: 0;
- left: 0;
- min-height: 50px;
- padding: 5px 0;
- font-size: 18px;
- line-height: 30px;
-
- .toggle-nav-collapse {
- left: 0;
- }
-
- .pin-nav-btn {
- right: 0;
- display: none;
-
- @media (min-width: $sidebar-breakpoint) {
- display: block;
- }
-
- .fa {
- transition: transform .15s;
-
- .page-sidebar-pinned & {
- transform: rotate(90deg);
- }
- }
- }
-}
-
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
@@ -161,57 +29,16 @@
}
}
-.page-sidebar-expanded {
- .sidebar-wrapper {
- width: $sidebar_width;
- }
-}
-
-.page-sidebar-pinned {
- .content-wrapper,
- .layout-nav {
- @media (min-width: $sidebar-breakpoint) {
- padding-left: $sidebar_width;
- }
- }
-
- .merge-request-tabs-holder.affix {
- @media (min-width: $sidebar-breakpoint) {
- left: $sidebar_width;
- }
- }
-
- &.right-sidebar-expanded {
- .line-resolve-all-container {
- @media (min-width: $sidebar-breakpoint) {
- display: none;
- }
- }
- }
-}
-
-header.header-sidebar-pinned {
- @media (min-width: $sidebar-breakpoint) {
- padding-left: ($sidebar_width + $gl-padding);
-
- .side-nav-toggle {
- display: none;
- }
-
- .header-content {
- padding-left: 0;
- }
- }
-}
-
.right-sidebar-collapsed {
padding-right: 0;
@media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
+ .content-wrapper {
+ padding-right: $gutter_collapsed_width;
+ }
.merge-request-tabs-holder.affix {
- right: $sidebar_collapsed_width;
+ right: $gutter_collapsed_width;
}
}
@@ -228,28 +55,31 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar):not(.wiki-sidebar) {
- padding-right: $sidebar_collapsed_width;
+ .content-wrapper {
+ padding-right: $gutter_collapsed_width;
}
}
@media (min-width: $screen-md-min) {
- padding-right: $gutter_width;
+ .content-wrapper {
+ padding-right: $gutter_width;
+ }
&:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
}
&.with-overlay .merge-request-tabs-holder.affix {
- right: $sidebar_collapsed_width;
+ right: $gutter_collapsed_width;
}
}
-
- &.with-overlay {
- padding-right: $sidebar_collapsed_width;
- }
}
.right-sidebar {
border-left: 1px solid $border-color;
+
+ &.affix {
+ position: fixed;
+ top: 0;
+ }
}
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..db5e2c51fe7 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/"],
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7809d4866f1..6841adb637e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,8 +1,6 @@
/*
* Layout
*/
-$sidebar_collapsed_width: 62px;
-$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
@@ -250,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
@@ -541,4 +539,13 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
-$action-icon-color: #d6d6d6; \ No newline at end of file
+$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/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 60ff72c703e..ea40f449134 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,6 +138,13 @@ pre {
margin: 0;
}
+blockquote {
+ color: $gl-grayish-blue;
+ padding: 0 0 0 15px;
+ margin: 0;
+ border-left: 3px solid $white-dark;
+}
+
span.highlight_word {
background-color: $highlighted-highlight-word !important;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index f2d60bff2b5..9a36d76136b 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -250,7 +250,7 @@
}
.issue-boards-search {
- width: 290px;
+ width: 395px;
.form-control {
display: inline-block;
@@ -298,12 +298,8 @@
.issue-boards-sidebar {
&.right-sidebar {
- top: 153px;
+ top: 0;
bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- top: 220px;
- }
}
.issuable-sidebar-header {
@@ -354,3 +350,171 @@
padding-right: 0;
}
}
+
+.add-issues-modal {
+ display: -webkit-flex;
+ display: flex;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba($black, .3);
+ z-index: 9999;
+}
+
+.add-issues-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 90vw;
+ height: 85vh;
+ max-width: 1100px;
+ min-height: 500px;
+ margin: auto;
+ padding: 25px 15px 0;
+ background-color: $white-light;
+ border-radius: $border-radius-default;
+ box-shadow: 0 2px 12px rgba($black, .5);
+
+ .empty-state {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ margin-top: 0;
+
+ &.add-issues-empty-state-filter {
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ -webkit-justify-content: center;
+ justify-content: center;
+ }
+
+ > .row {
+ width: 100%;
+ margin: auto 0;
+ }
+
+ .svg-content {
+ margin-top: -40px;
+ }
+ }
+}
+
+.add-issues-header {
+ margin: -25px -15px -5px;
+ border-top: 0;
+ border-bottom: 1px solid $border-color;
+ border-top-right-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+
+ > h2 {
+ margin: 0;
+ font-size: 18px;
+ }
+}
+
+.add-issues-search {
+ display: -webkit-flex;
+ display: flex;
+
+ .form-control {
+ margin-left: auto;
+
+ @media (min-width: $screen-sm-min) {
+ max-width: 200px;
+ }
+ }
+}
+
+.add-issues-list-column {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 50%;
+ }
+
+ @media (min-width: $screen-md-min) {
+ width: (100% / 3);
+ }
+}
+
+.add-issues-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ padding-top: 3px;
+ margin-left: -$gl-vert-padding;
+ margin-right: -$gl-vert-padding;
+ overflow-y: scroll;
+
+ .card-parent {
+ padding: 0 5px 5px;
+ }
+
+ .card {
+ border: 1px solid $border-gray-dark;
+ box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3);
+ cursor: pointer;
+ }
+}
+
+.add-issues-list-loading {
+ -webkit-align-self: center;
+ align-self: center;
+ width: 100%;
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ font-size: 35px;
+}
+
+.add-issues-footer {
+ margin: auto -15px 0;
+ padding-left: 15px;
+ padding-right: 15px;
+ border-bottom-right-radius: $border-radius-default;
+ border-bottom-left-radius: $border-radius-default;
+}
+
+.add-issues-footer-to-list {
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ line-height: 34px;
+}
+
+.issue-card-selected {
+ position: absolute;
+ right: -3px;
+ top: -3px;
+ width: 17px;
+ background-color: $blue-light;
+ color: $white-light;
+ border: 1px solid $border-blue-light;
+ font-size: 9px;
+ line-height: 15px;
+ border-radius: 50%;
+}
+
+.modal-filters {
+ display: flex;
+
+ > .dropdown {
+ display: none;
+ margin-right: 10px;
+
+ @media (min-width: $screen-sm-min) {
+ display: block;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100px;
+
+ @media (min-width: $screen-md-min) {
+ width: 140px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index fd101d43b5b..a24292a7c8c 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -91,7 +91,7 @@
}
&.scroll-top {
- top: 110px;
+ top: 10px;
}
&.scroll-bottom {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index fef8e8eec27..2029b6893ef 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -78,6 +78,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 +118,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;
}
@@ -159,7 +187,6 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index cda069e6c0e..5b777953fb0 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -284,7 +284,11 @@
.events-description {
line-height: 65px;
- padding-left: $gl-padding;
+ padding: 0 $gl-padding;
+ }
+
+ .events-info {
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 96ba7c40634..eab79c2a481 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -34,9 +34,14 @@
}
}
- .file-title {
+ .file-title,
+ .file-title-flex-parent {
cursor: pointer;
+ a:hover {
+ text-decoration: none;
+ }
+
&:hover {
background-color: $gray-normal;
}
@@ -84,6 +89,10 @@
.diff-line-num {
width: 50px;
+
+ a {
+ transition: none;
+ }
}
.line_holder td {
@@ -106,7 +115,7 @@
}
.add-diff-note {
- margin-left: -65px;
+ margin-left: -55px;
}
}
@@ -128,8 +137,13 @@
width: 35px;
font-weight: normal;
- &:hover {
- text-decoration: underline;
+ &[disabled] {
+ cursor: default;
+
+ &:hover,
+ &:active {
+ text-decoration: none;
+ }
}
}
}
@@ -480,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 778ef01430e..73a5da715f2 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -10,96 +10,102 @@
font-size: 34px;
}
-@media (max-width: $screen-xs-max) {
- .environments-container {
+.environments-folder-name {
+ font-weight: normal;
+ padding-top: 20px;
+}
+
+.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;
}
@@ -108,31 +114,108 @@
float: none;
}
}
- }
- .children-row .environment-name {
- margin-left: 17px;
- margin-right: -17px;
- }
+ .folder-icon {
+ margin-right: 3px;
+ color: $gl-text-color-secondary;
+ display: inline-block;
- .folder-icon {
- padding: 0 5px 0 0;
- }
+ .fa:nth-child(1) {
+ margin-right: 3px;
+ }
+ }
+
+ .folder-name {
+ cursor: pointer;
+ color: $gl-text-color-secondary;
+ display: inline-block;
+ }
- .folder-name {
- cursor: pointer;
+ .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;
}
-} \ No newline at end of file
+}
+
+.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 b989d72ce1c..08398bb43a2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -41,7 +41,6 @@
word-wrap: break-word;
.md {
- color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
@@ -156,7 +155,7 @@
@media (max-width: $screen-xs-max) {
.event-item {
- padding-left: $gl-padding;
+ padding-left: 0;
.event-title {
white-space: normal;
@@ -170,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/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4ef95d27f4f..4426169ef5a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -189,11 +189,10 @@
}
.right-sidebar {
- position: fixed;
+ position: absolute;
top: $header-height;
bottom: 0;
right: 0;
- z-index: 10;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -254,11 +253,11 @@
display: block;
}
- width: $sidebar_collapsed_width;
+ width: $gutter_collapsed_width;
padding-top: 0;
.block {
- width: $sidebar_collapsed_width - 2px;
+ width: $gutter_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;
@@ -461,8 +460,19 @@
.issuable-list {
li {
+
+ .issue-box {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ padding-right: $gl-padding;
+ }
+
.issue-check {
- float: left;
padding-right: $gl-padding;
margin-bottom: 10px;
min-width: 15px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8734a3b1598..b595480561b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,6 +1,6 @@
.issues-list {
.issue {
- padding: 10px $gl-padding;
+ padding: 10px 0 10px $gl-padding;
position: relative;
.title {
@@ -10,6 +10,11 @@
.issue-labels {
display: inline-block;
}
+
+ .icon-merge-request-unmerged {
+ height: 13px;
+ margin-bottom: 3px;
+ }
}
}
@@ -148,3 +153,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal;
}
}
+
+.recaptcha {
+ margin-bottom: 30px;
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 21d9b4c54ea..e1ef0b029a5 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,6 +116,22 @@
}
.manage-labels-list {
+ > li:not(.empty-message) {
+ background-color: $white-light;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+
+ &.sortable-ghost {
+ opacity: 0.3;
+ }
+ }
+
.btn-action {
color: $gl-text-color;
@@ -259,3 +275,8 @@
}
}
}
+
+.label-link {
+ display: inline-block;
+ vertical-align: text-top;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ab68b360f93..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 {
@@ -56,41 +57,101 @@
&.right {
float: right;
padding-right: 0;
+ }
- a {
- color: $gl-text-color;
- }
+ .modify-merge-commit-link {
+ color: $gl-text-color;
}
- .remove_source_checkbox {
+ .merge-param-checkbox {
margin: 0;
}
+
+ a .fa-question-circle {
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:focus {
+ color: $link-hover-color;
+ }
+ }
}
}
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+
+ i,
+ svg {
+ margin-right: 8px;
+ }
svg {
- margin-right: 4px;
position: relative;
top: 1px;
overflow: visible;
}
- &.ci-success_with_warnings {
+ & > span {
+ padding-right: 4px;
+ }
- i {
- color: $gl-warning;
- }
+ @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 {
+ flex-shrink: 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+
+ .ci-action-icon-wrapper {
+ line-height: 16px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ .stage-cell {
+ padding: 0 4px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ order: 1;
+ margin-top: $gl-padding-top;
+ border-radius: 3px;
+ background-color: $white-light;
+ border: 1px solid $gray-darker;
+ width: 100%;
+ text-align: center;
+
+ .dropdown-menu {
+ margin-left: -97.5px;
+ }
+
+ .arrow-up::before,
+ .arrow-up::after, {
+ margin-left: 97.5px;
+ }
+ }
}
.normal {
@@ -113,10 +174,6 @@
}
}
- p:last-child {
- margin-bottom: 0;
- }
-
.btn-grouped {
margin-left: 0;
margin-right: 7px;
@@ -179,8 +236,7 @@
.commit {
margin: 0;
- padding-top: 2px;
- padding-bottom: 2px;
+ padding: 10px 0;
list-style: none;
&:hover {
@@ -214,8 +270,15 @@
.mr-list {
.merge-request {
- padding: 10px 15px;
+ padding: 10px 0 10px 15px;
position: relative;
+ display: -webkit-flex;
+ display: flex;
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ }
.merge-request-title {
margin-bottom: 2px;
@@ -272,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 {
@@ -288,7 +404,7 @@
}
.panel-footer {
- padding: 5px 10px;
+ padding: 0;
.btn {
min-width: auto;
@@ -358,6 +474,11 @@
}
}
+.assign-to-me-link {
+ padding-left: 12px;
+ white-space: nowrap;
+}
+
.table-holder {
.ci-table {
@@ -369,6 +490,8 @@
}
.merged-buttons {
+ margin-top: 20px;
+
.btn {
float: left;
@@ -421,7 +544,7 @@
background-color: $white-light;
&.affix {
- top: 100px;
+ top: 0;
left: 0;
z-index: 10;
transition: right .15s;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 686b64cdd24..27c47d36818 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -30,6 +30,26 @@
word-wrap: break-word;
}
}
+
+ .panel-heading {
+ line-height: $line-height-base;
+ padding: 14px 16px;
+ display: -webkit-flex;
+ display: flex;
+
+ .title {
+ -webkit-flex: 1;
+ -webkit-flex-grow: 1;
+ flex: 1;
+ flex-grow: 2;
+ }
+
+ .counter {
+ -webkit-flex: 1;
+ flex: 0;
+ padding-left: 16px;
+ }
+ }
}
.milestone-summary {
@@ -178,3 +198,9 @@
}
}
}
+
+.issuable-row {
+ background-color: $white-light;
+ cursor: -webkit-grab;
+ cursor: grab;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f984b469609..c2156a5ac69 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -178,8 +178,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 da0caa30c26..e238f0865f6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -72,6 +72,7 @@ ul.notes {
overflow: hidden;
.system-note-commit-list-toggler {
+ color: $gl-link-color;
display: none;
padding: 10px 0 0;
cursor: pointer;
@@ -107,16 +108,6 @@ ul.notes {
display: none;
}
- p:last-child {
- a {
- color: $gl-text-color;
-
- &:hover {
- color: $gl-link-color;
- }
- }
- }
-
&::after {
content: '';
width: 100%;
@@ -340,6 +331,10 @@ ul.notes {
&:hover {
color: $gl-link-color;
+ }
+
+ &:focus,
+ &:hover {
text-decoration: none;
}
}
@@ -389,7 +384,7 @@ ul.notes {
top: 0;
.note-action-button {
- margin-left: 10px;
+ margin-left: 8px;
}
}
@@ -405,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) {
@@ -461,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: -4px;
- 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;
}
}
}
@@ -514,6 +509,7 @@ ul.notes {
}
.line-resolve-all-container {
+
.btn-group {
margin-left: -4px;
}
@@ -522,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 {
@@ -544,7 +561,6 @@ ul.notes {
}
.line-resolve-btn {
- display: inline-block;
position: relative;
top: 2px;
padding: 0;
@@ -567,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 cf79c2e36c2..20eabc83142 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,72 @@
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;
+ }
+
+ svg,
+ .fa {
+ margin-right: 0;
+ }
+ }
+
+ .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 +105,7 @@
@media (max-width: $screen-md-max) {
.content-list {
&.pipelines,
+ &.environments-container,
&.builds-content-list {
width: 100%;
overflow: auto;
@@ -61,27 +113,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,8 +129,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;
}
}
@@ -183,51 +226,11 @@
}
}
- .stage-cell {
- font-size: 0;
- padding: 10px 4px;
-
- > .stage-container > div > button > span > svg,
- > .stage-container > button > svg {
- height: 22px;
- width: 22px;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
- }
-
- .stage-container {
- display: inline-block;
- position: relative;
- margin-right: 6px;
-
- .tooltip {
- white-space: nowrap;
- }
-
- .tooltip-inner {
- padding: 3px 4px;
- }
-
- &:not(:last-child) {
- &::after {
- content: '';
- width: 7px;
- position: absolute;
- right: -7px;
- top: 10px;
- border-bottom: 2px solid $border-color;
- }
- }
- }
- }
-
.duration,
.finished-at {
color: $gl-text-color-secondary;
margin: 4px 0;
+ white-space: nowrap;
.fa {
font-size: 12px;
@@ -242,72 +245,57 @@
}
}
- .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-toggle,
- .dropdown-menu {
- color: $gl-text-color-secondary;
+ .build-link a {
+ color: $gl-text-color;
+ }
- .fa {
- color: $gl-text-color-secondary;
- font-size: 14px;
- }
+ .btn-group.open .dropdown-toggle {
+ box-shadow: none;
+ }
+}
- svg,
- .fa {
- margin-right: 0;
- }
- }
+.stage-cell {
+ font-size: 0;
+ padding: 10px 4px;
- .btn-remove {
- color: $white-light;
- }
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ height: 22px;
+ margin: 3px 6px 3px 0;
- .btn {
- .icon-play {
- height: 13px;
- width: 12px;
- }
- }
+ // Hack to show a button tooltip inline
+ button.has-tooltip + .tooltip {
+ min-width: 105px;
}
- .tooltip {
+ // Bootstrap way of showing the content inline for anchors.
+ a.has-tooltip {
white-space: nowrap;
}
- }
- .build-link {
-
- a {
- color: $gl-text-color;
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 7px;
+ position: absolute;
+ right: -7px;
+ top: 10px;
+ border-bottom: 2px solid $border-color;
+ }
}
}
-
- .btn-group.open .dropdown-toggle {
- box-shadow: none;
- }
}
.admin-builds-table {
@@ -322,31 +310,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;
}
}
@@ -665,7 +630,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: 200;
+ font-weight: normal;
}
// Dropdown button in mini pipeline graph
@@ -856,7 +821,7 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
- width: 90px;
+ max-width: 70%;
color: $gl-text-color-secondary;
margin-left: 2px;
display: inline-block;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 722b3006f7c..1a983d8c9ef 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -201,10 +201,6 @@
color: $note-disabled-comment-color;
}
-.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
- text-align: center;
-}
-
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
@@ -281,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/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 100ace41f2a..305feaacaa1 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,42 +1,3 @@
-.application-theme {
- label {
- margin-right: 20px;
- text-align: center;
-
- .preview {
- border-radius: 4px;
-
- height: 80px;
- margin-bottom: 10px;
- width: 160px;
-
- &.ui_blue {
- background: $theme-blue;
- }
-
- &.ui_charcoal {
- background: $theme-charcoal;
- }
-
- &.ui_graphite {
- background: $theme-graphite;
- }
-
- &.ui_black {
- background: $theme-black;
- }
-
- &.ui_green {
- background: $theme-green;
- }
-
- &.ui_violet {
- background: $theme-violet;
- }
- }
- }
-}
-
.syntax-theme {
label {
margin-right: 20px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8b59c20cb65..efa47be9a73 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -35,12 +35,8 @@
margin-bottom: 10px;
}
- .project-path {
- padding-right: 0;
-
- .form-control {
- border-radius: $border-radius-base;
- }
+ .project-path .form-control {
+ border-radius: $border-radius-base;
}
.input-group > div {
@@ -106,6 +102,7 @@
font-size: 24px;
font-weight: 400;
line-height: 1;
+ word-wrap: break-word;
.fa {
margin-left: 2px;
@@ -271,6 +268,13 @@
}
}
+.project-repo-buttons {
+ .project-action-button .dropdown-menu {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+}
+
.split-one {
display: inline-table;
margin-right: 12px;
@@ -490,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 {
@@ -634,14 +638,6 @@ 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;
@@ -649,30 +645,15 @@ pre.light-well {
}
.project-last-commit {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-base;
+ padding: 12px;
+
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
}
- &.container-fluid {
- padding-top: 12px;
- padding-bottom: 12px;
- background-color: $gray-light;
- border: 1px solid $border-color;
- border-right-width: 0;
- border-left-width: 0;
-
- @media (min-width: $screen-sm-min) {
- border-right-width: 1px;
- border-left-width: 1px;
- }
- }
-
- &.container-limited {
- @media (min-width: 1281px) {
- border-radius: $border-radius-base;
- }
- }
-
.ci-status {
margin-right: $gl-padding;
}
@@ -765,6 +746,8 @@ pre.light-well {
}
.protected-branches-list {
+ margin-bottom: 30px;
+
a {
color: $gl-text-color;
@@ -812,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 0d5604aae69..5f0aede4f5e 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -6,6 +6,8 @@
.navbar-nav {
li {
.badge.todos-pending-count {
+ position: inherit;
+ top: -6px;
margin-top: -5px;
font-weight: normal;
background: $todo-alert-blue;
@@ -43,6 +45,12 @@
}
}
+ .todo-avatar,
+ .todo-actions {
+ -webkit-flex: 0 0 auto;
+ flex: 0 0 auto;
+ }
+
.todo-actions {
display: -webkit-flex;
display: flex;
@@ -55,15 +63,49 @@
}
.todo-item {
- -webkit-flex: auto;
- flex: auto;
+ -webkit-flex: 0 1 100%;
+ flex: 0 1 100%;
+ min-width: 0;
+ }
+}
+
+.todos-list > .todo.todo-pending.done-reversible {
+ background-color: $gray-light;
+
+ &:hover {
+ border-color: $border-color;
+ }
+
+ .title {
+ font-weight: normal;
}
}
.todo-item {
.todo-title {
- @include str-truncated(calc(100% - 174px));
- overflow: visible;
+ display: flex;
+
+ & > .title-item {
+ -webkit-flex: 0 0 auto;
+ flex: 0 0 auto;
+ margin: 0 2px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .todo-label {
+ -webkit-flex: 0 1 auto;
+ flex: 0 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
}
.status-box {
@@ -128,7 +170,11 @@
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
- width: 135px;
+ width: 130px;
+ }
+
+ .dropdown-menu-toggle-sort {
+ width: auto;
}
}
}
@@ -142,10 +188,12 @@
.todo-item {
.todo-title {
- white-space: normal;
- overflow: visible;
- max-width: 100%;
+ flex-flow: row wrap;
margin-bottom: 10px;
+
+ .todo-label {
+ white-space: normal;
+ }
}
.todo-body {
@@ -156,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 948921efc0b..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: 200px;
- }
}
#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/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index d5783e14b21..9bc47bbe173 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -1,3 +1,11 @@
+.new-wiki-page {
+ .new-wiki-page-slug-tip {
+ display: inline-block;
+ max-width: 100%;
+ margin-top: 5px;
+ }
+}
+
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
@@ -9,12 +17,18 @@
@extend .top-area;
position: relative;
+ .wiki-breadcrumb {
+ border-bottom: 1px solid $white-normal;
+ padding: 11px 0;
+ }
+
.wiki-page-title {
margin: 0;
font-size: 22px;
}
.wiki-last-edit-by {
+ display: block;
color: $gl-text-color-secondary;
strong {
@@ -121,6 +135,10 @@
margin: 5px 0 10px;
}
+ ul.wiki-pages ul {
+ padding-left: 15px;
+ }
+
.wiki-sidebar-header {
padding: 0 $gl-padding $gl-padding;
@@ -129,3 +147,15 @@
}
}
}
+
+ul.wiki-pages-list.content-list {
+ & ul {
+ list-style: none;
+ margin-left: 0;
+ padding-left: 15px;
+ }
+
+ & ul li {
+ padding: 5px 0;
+ }
+}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 0ff3c3f5472..6cc1cc8e263 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info,
.file-title,
.file-holder,
-.sidebar-wrapper,
.nav,
.btn,
ul.notes-form,
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 543d5eac504..8d831ffdd70 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -83,6 +83,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,
@@ -109,6 +110,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:plantuml_url,
:max_artifacts_size,
:max_attachment_size,
+ :max_pages_size,
:metrics_enabled,
:metrics_host,
:metrics_method_call_threshold,
@@ -136,7 +138,11 @@ 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,
disabled_oauth_sign_in_sources: [],
import_sources: [],
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/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index 338496013a0..c09095b9849 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -2,5 +2,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController
def show
ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
+ @concurrency = Sidekiq.options[:concurrency]
end
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index c491e5c7550..8360ce08bdc 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,7 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.limit(10)
+ @projects = Project.with_route.limit(10)
@users = User.limit(10)
- @groups = Group.limit(10)
+ @groups = Group.with_route.limit(10)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index b7722a1d15d..cea3d088e94 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
- @groups = Group.with_statistics
+ @groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
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 b09ae423096..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
@@ -45,7 +54,7 @@ class Admin::ProjectsController < Admin::ApplicationController
protected
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index bc65dcc33d3..70ac6a75434 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
private
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:project_id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 7345c91f67d..348641e5ecb 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
- if @runner.update_attributes(runner_params)
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def resume
- if @runner.update_attributes(active: true)
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def pause
- if @runner.update_attributes(active: false)
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index ca04a17caa1..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',
@@ -21,12 +21,13 @@ class Admin::SystemInfoController < Admin::ApplicationController
'mqueue',
'proc',
'pstore',
+ 'rpc_pipefs',
'securityfs',
'sysfs',
'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 aa0f8d434dc..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
@@ -175,7 +182,7 @@ class Admin::UsersController < Admin::ApplicationController
def user_params_ce
[
- :admin,
+ :access_level,
:avatar,
:bio,
:can_create_group,
@@ -194,7 +201,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
- :theme_id,
:twitter,
:username,
:website_url
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bb47e2a8bf7..b7ce081a5cd 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
- before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
@@ -41,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
@@ -64,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
@@ -73,43 +76,21 @@ class ApplicationController < ActionController::Base
end
end
- def authenticate_user!(*args)
- if redirect_to_home_page_url?
- redirect_to current_application_settings.home_page_url and return
- end
-
- super(*args)
- end
-
def log_exception(exception)
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
application_trace.map!{ |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
- def reject_blocked!
- if current_user && current_user.blocked?
- sign_out current_user
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- redirect_to new_user_session_path
- end
- end
-
def after_sign_in_path_for(resource)
- if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked?
- sign_out resource
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- new_user_session_path
- else
- stored_location_for(:redirect) || stored_location_for(resource) || root_path
- end
+ stored_location_for(:redirect) || stored_location_for(resource) || root_path
end
def after_sign_out_path_for(resource)
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
@@ -145,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 and Gitlab.config.gitlab.port == 443
- headers['Strict-Transport-Security'] = 'max-age=31536000'
- end
end
def validate_user_service_ticket!
@@ -167,7 +144,7 @@ class ApplicationController < ActionController::Base
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
- redirect_to new_profile_password_path and return
+ return redirect_to new_profile_password_path
end
end
@@ -196,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
@@ -233,7 +210,7 @@ class ApplicationController < ActionController::Base
def require_email
if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
- redirect_to profile_path, notice: 'Please complete your profile with email address' and return
+ return redirect_to profile_path, notice: 'Please complete your profile with email address'
end
end
@@ -302,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 6f43ce5226d..9ac8197e45a 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -5,22 +5,27 @@ module CreatesCommit
set_commit_variables
commit_params = @commit_params.merge(
- source_project: @project,
- source_branch: @ref,
- target_branch: @target_branch
+ start_project: @mr_target_project,
+ start_branch: @mr_target_branch,
+ target_branch: @mr_source_branch
)
- result = service.new(@tree_edit_project, current_user, commit_params).execute
+ result = service.new(
+ @mr_source_project, current_user, commit_params).execute
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
@@ -56,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
@@ -89,38 +98,27 @@ module CreatesCommit
@mr_source_project != @mr_target_project
end
- def different_branch?
- @mr_source_branch != @mr_target_branch || different_project?
- end
-
def create_merge_request?
- params[:create_merge_request].present? && different_branch?
+ # 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? || @mr_target_branch != @mr_source_branch)
end
def set_commit_variables
- @mr_source_branch ||= @target_branch
-
if can?(current_user, :push_code, @project)
- # Edit file in this project
- @tree_edit_project = @project
@mr_source_project = @project
-
- if @project.forked?
- # Merge request from this project to fork origin
- @mr_target_project = @project.forked_from_project
- @mr_target_branch = @mr_target_project.repository.root_ref
- else
- # Merge request to this project
- @mr_target_project = @project
- @mr_target_branch ||= @ref
- end
+ @target_branch ||= @ref
else
- # Edit file in fork
- @tree_edit_project = current_user.fork_of(@project)
- # Merge request from fork to this project
- @mr_source_project = @tree_edit_project
- @mr_target_project = @project
- @mr_target_branch ||= @ref
+ @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 = @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 6247934f81e..85ae4985e58 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,6 +9,36 @@ module IssuableCollections
private
+ 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_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 }
+ 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,
+ merge_requests.try(:last).to_i
+ )
+ end
+ end
+
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b46adcceb60..b17c138d5c7 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -9,6 +9,9 @@ module IssuesAction
.non_archived
.page(params[:page])
+ @collection_type = "Issue"
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index fdb05bb3228..d3c8e4888bc 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.page(params[:page])
+
+ @collection_type = "MergeRequest"
+ @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..2992568ae66 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -33,6 +33,7 @@ module ServiceParams
:issues_url,
:jira_issue_transition_id,
:merge_requests_events,
+ :mock_service_url,
:namespace,
:new_issue_url,
:notify,
@@ -59,10 +60,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/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
new file mode 100644
index 00000000000..ca6dffe1cc5
--- /dev/null
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -0,0 +1,21 @@
+module SnippetsActions
+ extend ActiveSupport::Concern
+
+ def edit
+ end
+
+ def raw
+ send_data(
+ convert_line_endings(@snippet.content),
+ type: 'text/plain; charset=utf-8',
+ disposition: 'inline',
+ filename: @snippet.sanitized_file_name
+ )
+ end
+
+ private
+
+ def convert_line_endings(content)
+ params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 562f92bd83c..d0a692070d9 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -1,6 +1,8 @@
module SpammableActions
extend ActiveSupport::Concern
+ include Recaptcha::Verify
+
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
end
@@ -15,6 +17,33 @@ module SpammableActions
private
+ def recaptcha_check_with_fallback(&fallback)
+ if spammable.valid?
+ redirect_to spammable
+ elsif render_recaptcha?
+ if params[:recaptcha_verification]
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ render :verify
+ else
+ yield
+ end
+ end
+
+ def spammable_params
+ default_params = { request: request }
+
+ recaptcha_check = params[:recaptcha_verification] &&
+ Gitlab::Recaptcha.load_configurations! &&
+ verify_recaptcha
+
+ return default_params unless recaptcha_check
+
+ { recaptcha_verified: true,
+ spam_log_id: params[:spam_log_id] }.merge(default_params)
+ end
+
def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
@@ -22,4 +51,11 @@ module SpammableActions
def authorize_submit_spammable!
access_denied! unless current_user.admin?
end
+
+ def render_recaptcha?
+ return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
+ return false unless Gitlab::Recaptcha.enabled?
+
+ spammable.spam
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index de6bc689bb7..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).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 c08eb811532..325ae565537 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,21 +1,14 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
- before_action :event_filter
-
def index
- @projects = current_user.authorized_projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.includes(:namespace)
+ @projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
- @last_push = current_user.recent_push
-
respond_to do |format|
- format.html
+ format.html { @last_push = current_user.recent_push }
format.atom do
- event_filter
load_events
render layout: false
end
@@ -28,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
- @projects = current_user.viewable_starred_projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.includes(:namespace, :forked_from_project, :tags)
+ @projects = load_projects(current_user.viewable_starred_projects)
+ @projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@@ -39,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format|
format.html
-
format.json do
render json: {
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
@@ -50,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
+ def load_projects(base_scope)
+ projects = base_scope.sorted_by_activity.includes(:namespace)
+
+ filter_projects(projects)
+ end
+
def load_events
- @events = Event.in_projects(@projects)
- @events = @event_filter.apply_filter(@events).with_associations
+ @events = Event.in_projects(load_projects(current_user.authorized_projects))
+ @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index e3933e3d7b1..5848ca62777 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,4 +1,6 @@
class Dashboard::TodosController < Dashboard::ApplicationController
+ include ActionView::Helpers::NumberHelper
+
before_action :find_todos, only: [:index, :destroy_all]
def index
@@ -29,6 +31,17 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
end
+ def restore
+ TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user)
+
+ render json: todos_counts
+ end
+
+ # Used in TodosHelper also
+ def self.todos_count_format(count)
+ count >= 100 ? '99+' : count
+ end
+
private
def find_todos
@@ -37,8 +50,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def todos_counts
{
- count: current_user.todos_pending_count,
- done_count: current_user.todos_done_count
+ count: number_with_delimiter(current_user.todos_pending_count),
+ done_count: number_with_delimiter(current_user.todos_done_count)
}
end
end
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/application_controller.rb b/app/controllers/explore/application_controller.rb
index a1ab8b99048..baf54520b9c 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,5 +1,5 @@
class Explore::ApplicationController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'explore'
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/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 4f273a8d4f0..0cbf3eb58a3 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
- @members = @group.group_members
+ @members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
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 264b14713fb..05f9ee1ee90 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
+ before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
+ before_action :user_actions, only: [:show, :subgroups]
+
layout :determine_layout
def index
@@ -30,20 +32,19 @@ 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
end
def show
- if current_user
- @last_push = current_user.recent_push
- @notification_setting = current_user.notification_settings_for(group)
- end
-
- @nested_groups = group.children
-
setup_projects
respond_to do |format|
@@ -62,6 +63,11 @@ class GroupsController < Groups::ApplicationController
end
end
+ def subgroups
+ @nested_groups = group.children
+ @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
+ end
+
def activity
respond_to do |format|
format.html
@@ -91,7 +97,7 @@ class GroupsController < Groups::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
@@ -99,17 +105,20 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
+ options = {}
+ options[:only_owned] = true if params[:shared] == '0'
+ options[:only_shared] = true if params[:shared] == '1'
+
+ @projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = @projects.includes(:namespace)
@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?
-
- @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
+ @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
@@ -138,7 +147,10 @@ class GroupsController < Groups::ApplicationController
:public,
:request_access_enabled,
:share_with_group_lock,
- :visibility_level
+ :visibility_level,
+ :parent_id,
+ :create_chat_team,
+ :chat_team_name
]
end
@@ -147,4 +159,11 @@ class GroupsController < Groups::ApplicationController
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def user_actions
+ if current_user
+ @last_push = current_user.recent_push
+ @notification_setting = current_user.notification_settings_for(group)
+ end
+ end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 37feff79999..87c0f8905ff 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,5 +1,5 @@
class HelpController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'help'
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 99b10b2f9b3..5df6bd34185 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -29,7 +29,7 @@ class Import::FogbugzController < Import::BaseController
unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? }
flash.now[:alert] = 'All users must have a name.'
- render 'new_user_map' and return
+ return render 'new_user_map'
end
session[:fogbugz_user_map] = user_map
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 8d0de158f98..7d7f13ce5d5 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -44,13 +44,13 @@ class Import::GoogleCodeController < Import::BaseController
rescue
flash.now[:alert] = "The entered user map is not a valid JSON user map."
- render "new_user_map" and return
+ return render "new_user_map"
end
unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
flash.now[:alert] = "The entered user map is not a valid JSON user map."
- render "new_user_map" and return
+ return render "new_user_map"
end
# This is the default, so let's not save it into the database.
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 58964a0e65d..7625187c7be 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -42,9 +42,7 @@ class InvitesController < ApplicationController
@token = params[:id]
@member = Member.find_by_invite_token(@token)
- unless @member
- render_404 and return
- end
+ return render_404 unless @member
@member
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/koding_controller.rb b/app/controllers/koding_controller.rb
index f3759b4c0ea..6b1e64ce819 100644
--- a/app/controllers/koding_controller.rb
+++ b/app/controllers/koding_controller.rb
@@ -1,5 +1,5 @@
class KodingController < ApplicationController
- before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ before_action :check_integration!
layout 'koding'
def index
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/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index f54c79c2e37..58d50ad647b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_omniauth
end
+ def authentiq
+ if params['sid']
+ handle_service_ticket oauth['provider'], params['sid']
+ end
+ handle_omniauth
+ end
+
private
def handle_omniauth
@@ -115,7 +122,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
else
error_message = @user.errors.full_messages.to_sentence
- redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
+ return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 830e0b9591b..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)
@@ -45,13 +40,13 @@ class Profiles::KeysController < Profiles::ApplicationController
if user.present?
render text: user.all_ssh_keys.join("\n"), content_type: "text/plain"
else
- render_404 and return
+ return render_404
end
rescue => e
render text: e.message
end
else
- render_404 and return
+ return render_404
end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index b8b71d295f6..a271e2dfc4b 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)
+ params.require(:user).permit(:notification_email, :notified_of_own_activity)
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/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index a9a06ecc808..0d891ef4004 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout,
:dashboard,
:project_view,
- :theme_id
)
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/application_controller.rb b/app/controllers/projects/application_controller.rb
index b2ff36f6538..e2f81b09adc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -18,13 +18,13 @@ class Projects::ApplicationController < ApplicationController
# to
# localhost/group/project
#
- if id =~ /\.git\Z/
+ if params[:format] == 'git'
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
return
end
project_path = "#{namespace}/#{id}"
- @project = Project.find_with_namespace(project_path)
+ @project = Project.find_by_full_path(project_path)
if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
@@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
- @show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
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/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9940263ae24..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,13 +23,17 @@ 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
def show
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end
def edit
@@ -38,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))
@@ -59,10 +63,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
- success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
- failure_view: :show,
- failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+ create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
+ success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) },
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
@@ -85,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))
@@ -93,7 +102,7 @@ class Projects::BlobController < Projects::ApplicationController
else
if tree = @repository.tree(@commit.id, @path)
if tree.entries.any?
- redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path)) and return
+ return redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path))
end
end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index dc33e1405f2..28c9646910d 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -7,7 +7,8 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
- issues = issues.page(params[:page])
+ 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)
@@ -59,11 +66,11 @@ module Projects
end
def filter_params
- params.merge(board_id: params[:board_id], id: params[:list_id])
+ params.merge(board_id: params[:board_id], id: params[:list_id]).compact
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: [: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..22714d9c5a4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,25 +1,27 @@
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
+ @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
respond_to do |format|
- format.html
+ format.html do
+ @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
+ render json: @branches.map(&:name)
end
end
end
@@ -32,6 +34,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 +46,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 +87,19 @@ 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 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 c871043efbd..cc67f688d51 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController
format.json do
render json: PipelineSerializer
.new(project: @project, user: @current_user)
- .with_pagination(request, response)
.represent(@pipelines)
end
end
@@ -50,25 +49,37 @@ class Projects::CommitController < Projects::ApplicationController
end
def revert
- assign_change_commit_vars(@commit.revert_branch_name)
+ 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(@commit.cherry_pick_branch_name)
+ assign_change_commit_vars
+
+ return render_404 if @start_branch.blank?
+
+ @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
- return render_404 if @target_branch.blank?
+ @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
@@ -79,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
@@ -95,6 +106,8 @@ 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
def define_note_vars
@@ -116,14 +129,8 @@ class Projects::CommitController < Projects::ApplicationController
}
end
- def assign_change_commit_vars(mr_source_branch)
- @commit = project.commit(params[:id])
- @target_branch = params[:target_branch]
- @mr_source_branch = mr_source_branch
- @mr_target_branch = @target_branch
- @commit_params = {
- commit: @commit,
- create_merge_request: params[:create_merge_request].present? || different_project?
- }
+ def assign_change_commit_vars
+ @start_branch = params[:start_branch]
+ @commit_params = { commit: @commit }
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d32966645c8..c6651254d70 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -46,7 +46,8 @@ class Projects::CompareController < Projects::ApplicationController
end
def define_diff_vars
- @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
+ @compare = CompareService.new(@project, @head_ref)
+ .execute(@project, @start_ref)
if @compare
@commits = @compare.commits
@@ -56,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options)
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@diff_notes_disabled = true
@grouped_diff_discussions = {}
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 87cc36253f1..fa37963dfd4 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -5,19 +5,44 @@ 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
- @scope = params[:scope]
@environments = project.environments
+ .with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
- render json: EnvironmentSerializer
- .new(project: @project, user: current_user)
- .represent(@environments)
+ render json: {
+ environments: EnvironmentSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .within_folders
+ .represent(@environments),
+ available_count: project.environments.available.count,
+ stopped_count: project.environments.stopped.count
+ }
+ end
+ end
+ end
+
+ def folder
+ folder_environments = project.environments.where(environment_type: params[:id])
+ @environments = folder_environments.with_state(params[:scope] || :available)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ environments: EnvironmentSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .represent(@environments),
+ available_count: folder_environments.available.count,
+ stopped_count: folder_environments.stopped.count
+ }
end
end
end
@@ -52,10 +77,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
- return render_404 unless @environment.stoppable?
+ return render_404 unless @environment.available?
- new_action = @environment.stop!(current_user)
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
+ stop_action = @environment.stop_with_action!(current_user)
+
+ if stop_action
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ else
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ end
end
def terminal
@@ -79,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 70845617d3c..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_with_namespace("#{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 8472ceca329..f2fee62ebd6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- @issues = issues_collection
- @issues = @issues.page(params[:page])
+ @collection_type = "Issue"
+ @issues = issues_collection
+ @issues = @issues.page(params[:page])
+ @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))
end
@@ -61,8 +64,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
@@ -91,17 +101,25 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- extra_params = { request: request,
- merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
- @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
+ 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]
+ )
+
+ 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
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :new
- end
+ recaptcha_check_with_fallback { render :new }
end
format.js do
@link = @issue.attachment.url.to_js
@@ -110,7 +128,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def update
- @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
+ update_params = issue_params.merge(spammable_params)
+
+ @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
@@ -122,11 +142,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html do
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :edit
- end
+ recaptcha_check_with_fallback { render :edit }
end
format.json do
@@ -135,8 +151,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def referenced_merge_requests
@@ -187,14 +202,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/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 440259b643c..8a5a645ed0e 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
+
+ if Guest.can?(:download_code, project)
+ object[:authenticated] = true
+ end
else
object[:error] = {
code: 404,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 6eb542e4bd8..82f9b6e06db 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]
@@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
- @merge_requests = merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page])
+ @collection_type = "MergeRequest"
+ @merge_requests = merge_requests_collection
+ @merge_requests = @merge_requests.page(params[:page])
+ @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))
end
@@ -47,6 +50,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, labels_params).execute
end
+ @users = []
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
+
respond_to do |format|
format.html
format.json do
@@ -103,6 +117,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ @environment = @merge_request.environments_for(current_user).last
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@@ -216,25 +232,33 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
format.json do
- render json: {
- html: view_to_html_string('projects/merge_requests/show/_pipelines'),
- pipelines: PipelineSerializer
- .new(project: @project, user: @current_user)
- .with_pagination(request, response)
- .represent(@pipelines)
- }
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
end
end
end
def new
- define_new_vars
+ respond_to do |format|
+ format.html { define_new_vars }
+ format.json do
+ define_pipelines_vars
+
+ render json: {
+ pipelines: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ }
+ end
+ end
end
def new_diffs
respond_to do |format|
format.html do
define_new_vars
+ @show_changes_tab = true
render "new"
end
format.json do
@@ -245,7 +269,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@diff_notes_disabled = true
- render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) }
end
end
end
@@ -272,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: { 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
@@ -299,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
@@ -316,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
@@ -330,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
@@ -341,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
@@ -357,11 +383,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
- if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
- @status = :success
- elsif merge_request.merge_when_build_succeeds
- @status = :merge_when_build_succeeds
- 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
@@ -417,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
@@ -435,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
@@ -444,14 +476,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status
environments =
begin
- @merge_request.environments.map do |environment|
- next unless can?(current_user, :read_environment, environment)
-
+ @merge_request.environments_for(current_user).map do |environment|
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
- if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
@@ -603,6 +633,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
+ @show_changes_tab = params[:show_changes].present?
+
define_pipelines_vars
end
@@ -645,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 c5d93ce25bc..d00177e7612 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy
if note.editable?
- Notes::DeleteService.new(project, current_user).execute(note)
+ Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
@@ -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/pages_controller.rb b/app/controllers/projects/pages_controller.rb
new file mode 100644
index 00000000000..fbd18b68141
--- /dev/null
+++ b/app/controllers/projects/pages_controller.rb
@@ -0,0 +1,22 @@
+class Projects::PagesController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_read_pages!, only: [:show]
+ before_action :authorize_update_pages!, except: [:show]
+
+ def show
+ @domains = @project.pages_domains.order(:domain)
+ end
+
+ def destroy
+ project.remove_pages
+ project.pages_domains.destroy_all
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Pages were removed')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
new file mode 100644
index 00000000000..b8c253f6ae3
--- /dev/null
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -0,0 +1,49 @@
+class Projects::PagesDomainsController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_update_pages!, except: [:show]
+ before_action :domain, only: [:show, :destroy]
+
+ def show
+ end
+
+ def new
+ @domain = @project.pages_domains.new
+ end
+
+ def create
+ @domain = @project.pages_domains.create(pages_domain_params)
+
+ if @domain.valid?
+ redirect_to namespace_project_pages_path(@project.namespace, @project)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ @domain.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Domain was removed')
+ end
+ format.js
+ end
+ end
+
+ private
+
+ def pages_domain_params
+ params.require(:pages_domain).permit(
+ :certificate,
+ :key,
+ :domain
+ )
+ end
+
+ def domain
+ @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
+ 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/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 53ce23221ed..c8c80551ac9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
- @ref = params[:ref] || @project.default_branch || 'master'
-
- @badges = [Gitlab::Badge::Build::Status,
- Gitlab::Badge::Coverage::Report]
-
- @badges.map! do |badge|
- badge.new(@project, @ref).metadata
- end
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params)
end
def update
if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else
render 'show'
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 9a438d5512c..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,20 +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: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
- merge_access_levels: 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/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 53c36635efe..8b50ea207a5 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,18 +5,14 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @project_runners = project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners.
- assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
- @shared_runners_count = @shared_runners.count(:all)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
end
def update
- if @runner.update_attributes(runner_params)
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
render 'edit'
@@ -32,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def resume
- if @runner.update_attributes(active: true)
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
@@ -40,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def pause
- if @runner.update_attributes(active: false)
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
- redirect_to namespace_project_runners_path(project.namespace, project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
protected
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/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
new file mode 100644
index 00000000000..6f009d61950
--- /dev/null
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -0,0 +1,44 @@
+module Projects
+ module Settings
+ class CiCdController < Projects::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ end
+
+ private
+
+ def define_runners_variables
+ @project_runners = @project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def define_secret_variables
+ @variable = Ci::Variable.new
+ end
+
+ def define_triggers_variables
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def define_badges_variables
+ @ref = params[:ref] || @project.default_branch || 'master'
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
+ end
+ end
+ end
+end
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/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 5d193f26a8e..ea1a97b7cf0 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,7 @@
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
include SpammableActions
+ include SnippetsActions
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -37,27 +38,19 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- create_params = snippet_params.merge(request: request)
- @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
+ create_params = snippet_params.merge(spammable_params)
- if @snippet.valid?
- respond_with(@snippet,
- location: namespace_project_snippet_path(@project.namespace,
- @project, @snippet))
- else
- render :new
- end
- end
+ @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
- def edit
+ recaptcha_check_with_fallback { render :new }
end
def update
- UpdateSnippetService.new(project, current_user, @snippet,
- snippet_params).execute
- respond_with(@snippet,
- location: namespace_project_snippet_path(@project.namespace,
- @project, @snippet))
+ update_params = snippet_params.merge(spammable_params)
+
+ UpdateSnippetService.new(project, current_user, @snippet, update_params).execute
+
+ recaptcha_check_with_fallback { render :edit }
end
def show
@@ -74,15 +67,6 @@ class Projects::SnippetsController < Projects::ApplicationController
redirect_to namespace_project_snippets_path(@project.namespace, @project)
end
- def raw
- send_data(
- @snippet.content,
- type: 'text/plain; charset=utf-8',
- disposition: 'inline',
- filename: @snippet.sanitized_file_name
- )
- end
-
protected
def snippet
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index e2d9d5ed460..ea7e4d9f663 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
end
def create
- result = CreateTagService.new(@project, current_user).
+ result = Tags::CreateService.new(@project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@@ -41,13 +41,27 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
- DeleteTagService.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/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index cb3ed0f6f9c..4f094146348 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -15,10 +15,10 @@ class Projects::TreeController < Projects::ApplicationController
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
- redirect_to(
+ return redirect_to(
namespace_project_blob_path(@project.namespace, @project,
File.join(@ref, @path))
- ) and return
+ )
elsif @path.present?
return render_404
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 92359745cec..c47198c5eb6 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,34 +1,77 @@
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'
def index
- @triggers = project.triggers
- @trigger = Ci::Trigger.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
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_triggers_path(@project.namespace, @project)
+ flash[:notice] = 'Trigger was created successfully.'
else
- @triggers = project.triggers.select(&:persisted?)
- render :index
+ 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
+ if trigger.destroy
+ flash[:notice] = "Trigger removed."
+ else
+ flash[:alert] = "Could not remove the trigger."
+ end
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ 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/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e617be8f9fb..61686499bd3 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,6 +1,6 @@
class Projects::UploadsController < Projects::ApplicationController
- skip_before_action :reject_blocked!, :project,
- :repository, if: -> { action_name == 'show' && image_or_video? }
+ skip_before_action :project, :repository,
+ if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
@@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController
namespace = params[:namespace_id]
id = params[:project_id]
- file_project = Project.find_with_namespace("#{namespace}/#{id}")
+ file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 6f068729390..a4d1b1ee69b 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
def index
- @variable = Ci::Variable.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def show
@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController
@variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ flash[:notice] = 'Variables were successfully updated.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project)
else
- render action: "index"
+ render "show"
end
end
@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController
@key = @project.variables.find(params[:id])
@key.destroy
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.'
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c3353446fd1..2d8064c9878 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
+ @wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
def show
@@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
- @page.delete if @page
+ WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home),
@@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- @sidebar_wiki_pages = @project_wiki.pages.first(15)
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 444ff837bb3..3e2015b7d5e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -231,12 +231,16 @@ class ProjectsController < Projects::ApplicationController
end
def refs
+ branches = BranchesFinder.new(@repository, params).execute.map(&:name)
+
options = {
- 'Branches' => @repository.branch_names,
+ 'Branches' => branches.take(100),
}
unless @repository.tag_count.zero?
- options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ tags = TagsFinder.new(@repository, params).execute.map(&:name)
+
+ options['Tags'] = tags.take(100)
end
# If reference is commit id - we should add it to branch/tag selectbox
@@ -310,7 +314,7 @@ 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,
:path,
:public_builds,
:request_access_enabled,
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index bf27f3d4d51..b44f38d4a0c 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -17,20 +17,20 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
- flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy
- DeleteUserService.new(current_user).execute(current_user)
+ Users::DestroyService.new(current_user).execute(current_user)
respond_to do |format|
format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
- end
+ end
end
end
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/search_controller.rb b/app/controllers/search_controller.rb
index 6576ebd5235..612d69cf557 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
include SearchHelper
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 b169d993688..f3fd3da8b20 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,6 +1,7 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
include SpammableActions
+ include SnippetsActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -22,13 +23,14 @@ class SnippetsController < ApplicationController
if params[:username].present?
@user = User.find_by(username: params[:username])
- render_404 and return unless @user
+ return render_404 unless @user
@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
@@ -41,19 +43,19 @@ class SnippetsController < ApplicationController
end
def create
- create_params = snippet_params.merge(request: request)
- @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
+ create_params = snippet_params.merge(spammable_params)
- respond_with @snippet.becomes(Snippet)
- end
+ @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
- def edit
+ recaptcha_check_with_fallback { render :new }
end
def update
- UpdateSnippetService.new(nil, current_user, @snippet,
- snippet_params).execute
- respond_with @snippet.becomes(Snippet)
+ update_params = snippet_params.merge(spammable_params)
+
+ UpdateSnippetService.new(nil, current_user, @snippet, update_params).execute
+
+ recaptcha_check_with_fallback { render :edit }
end
def show
@@ -67,18 +69,9 @@ class SnippetsController < ApplicationController
redirect_to snippets_path
end
- def raw
- send_data(
- @snippet.content,
- type: 'text/plain; charset=utf-8',
- disposition: 'inline',
- filename: @snippet.sanitized_file_name
- )
- end
-
def download
send_data(
- @snippet.content,
+ convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
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/environments_finder.rb b/app/finders/environments_finder.rb
new file mode 100644
index 00000000000..a59f8c1efa3
--- /dev/null
+++ b/app/finders/environments_finder.rb
@@ -0,0 +1,55 @@
+class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids).order_by_last_deployed_at.to_a
+
+ environments.select! do |environment|
+ Ability.allowed?(current_user, :read_environment, environment)
+ end
+
+ if ref && commit
+ environments.select! do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
+ if ref && params[:recently_updated]
+ environments.select! do |environment|
+ environment.recently_updated_on_branch?(ref)
+ end
+ end
+
+ environments
+ end
+
+ private
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
new file mode 100644
index 00000000000..fce3775f40e
--- /dev/null
+++ b/app/finders/group_members_finder.rb
@@ -0,0 +1,20 @@
+class GroupMembersFinder
+ def initialize(group)
+ @group = group
+ end
+
+ def execute
+ group_members = @group.members
+
+ return group_members unless @group.parent
+
+ parents_members = GroupMember.non_request.
+ where(source_id: @group.ancestors.select(:id)).
+ where.not(user_id: @group.users.select(:id))
+
+ wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+
+ GroupMember.where(wheres.join(' OR '))
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index aa8f4c1d0e4..3b9a421b118 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder
projects = []
if current_user
- if @group.users.include?(current_user) || current_user.admin?
+ if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 4e43f42e9e1..d932a17883f 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder
def execute(current_user = nil)
segments = all_groups(current_user)
- find_union(segments, Group).order_id_desc
+ find_union(segments, Group).with_route.order_id_desc
end
private
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 1576fc80a6b..2fca012252e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -16,9 +16,10 @@
# label_name: string
# sort: string
# non_archived: boolean
+# iids: integer[]
#
class IssuableFinder
- NONE = '0'
+ NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -32,14 +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
@@ -105,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 =
@@ -115,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)
@@ -255,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
@@ -266,16 +269,11 @@ class IssuableFinder
end
def by_search(items)
- if search
- items =
- if search =~ iid_pattern
- items.where(iid: $~[:iid])
- else
- items.full_search(search)
- end
- end
+ search ? items.full_search(search) : items
+ end
- items
+ def by_iids(items)
+ params[:iids].present? ? items.where(iid: params[:iids]) : items
end
def sort(items)
@@ -317,13 +315,14 @@ class IssuableFinder
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)
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
@@ -337,9 +336,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
@@ -399,4 +399,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 707eddd4d29..08713272947 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -26,10 +26,6 @@ class IssuesFinder < IssuableFinder
IssuesFinder.not_restricted_by_confidentiality(current_user)
end
- def iid_pattern
- @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
- end
-
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
@@ -45,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 8b82255445e..1eec45d9cb5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -23,11 +23,7 @@ class MergeRequestsFinder < IssuableFinder
private
- def iid_pattern
- @iid_pattern ||= %r{\A[
- #{Regexp.escape(MergeRequest.reference_prefix)}
- #{Regexp.escape(Issue.reference_prefix)}
- ](?<iid>\d+)\z
- }x
+ 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/projects_finder.rb b/app/finders/projects_finder.rb
index c7911736812..18ec45f300d 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
- find_union(segments, Project)
+ find_union(segments, Project).with_route
end
private
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index a93a63bdb9b..b7f091f334d 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
@@ -99,7 +99,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 a112928c6de..a3213581498 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -37,7 +37,7 @@ module ApplicationHelper
if project_id.is_a?(Project)
project_id
else
- Project.find_with_namespace(project_id)
+ Project.find_by_full_path(project_id)
end
if project.avatar_url
@@ -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,
@@ -296,4 +297,13 @@ module ApplicationHelper
def page_class
"issue-boards-page" if current_controller?(:boards)
end
+
+ # Returns active css class when condition returns true
+ # otherwise returns nil.
+ #
+ # Example:
+ # %li{ class: active_when(params[:filter] == '1') }
+ def active_when(condition)
+ 'active' if condition
+ end
end
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..7f32c1b5300 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
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 38c586ccd31..f43827da446 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -6,7 +6,9 @@ module BoardsHelper
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}",
- issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+ issue_link_base: namespace_project_issues_path(@project.namespace, @project),
+ root_path: root_path,
+ bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project),
}
end
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 9fc69e12266..2fcb7a59fc3 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -1,7 +1,7 @@
module BuildsHelper
def sidebar_build_class(build, current_build)
build_class = ''
- build_class += ' active' if build == current_build
+ build_class += ' active' if build.id === current_build.id
build_class += ' retried' if build.retried?
build_class
end
@@ -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..a7cdca9ba2e 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
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 6dcb624c4da..8aad39e148b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -194,7 +194,7 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff_new_path, project)
+ def view_file_button(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
@@ -205,6 +205,17 @@ module CommitsHelper
end
end
+ def view_on_environment_button(commit_sha, diff_new_path, environment)
+ return unless environment && commit_sha
+
+ 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
+ icon('external-link')
+ end
+ end
+
def truncate_sha(sha)
Commit.truncate_sha(sha)
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..5605393c0c3 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 b pre code p span),
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style']
+ )
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_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 2159e4ce21a..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
@@ -211,8 +215,12 @@ module GitlabRoutingHelper
def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args)
end
-
+
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
+
+ def project_settings_ci_cd_path(project, *args)
+ namespace_project_settings_ci_cd_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03354c235eb..aad83731b87 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
}
}
@@ -95,8 +97,23 @@ module IssuablesHelper
h(milestone_title.presence || 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 +215,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 0e456214d37..68c09c922a6 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -1,5 +1,9 @@
module JavascriptHelper
def page_specific_javascript_tag(js)
- javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
+ javascript_include_tag asset_path(js)
+ end
+
+ def page_specific_javascript_bundle_tag(js)
+ javascript_include_tag(*webpack_asset_paths(js))
end
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 8c2c4e8833b..38be073c8dc 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -20,8 +20,8 @@ module MergeRequestsHelper
end
def mr_widget_refresh_url(mr)
- if mr && mr.source_project
- merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
+ if mr && mr.target_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else
''
end
@@ -64,21 +64,21 @@ module MergeRequestsHelper
end
def mr_closes_issues
- @mr_closes_issues ||= @merge_request.closes_issues
+ @mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
def mr_issues_mentioned_but_not_closing
- @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
merge_request: {
- source_project_id: @merge_request.source_project_id,
- target_project_id: @merge_request.target_project_id,
- source_branch: @merge_request.source_branch,
- target_branch: @merge_request.target_branch,
+ source_project_id: merge_request.source_project_id,
+ target_project_id: merge_request.target_project_id,
+ source_branch: merge_request.source_branch,
+ target_branch: merge_request.target_branch,
},
change_branches: true
)
@@ -143,4 +143,16 @@ module MergeRequestsHelper
def different_base?(version1, version2)
version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
end
+
+ def merge_params(merge_request)
+ {
+ merge_when_pipeline_succeeds: true,
+ should_remove_source_branch: true,
+ sha: merge_request.diff_head_sha
+ }.merge(merge_params_ee(merge_request))
+ end
+
+ def merge_params_ee(merge_request)
+ {}
+ end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 729928ce1dd..7011e670cee 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -97,7 +97,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 e0b8dc1393b..2e3a15bc1b9 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,4 +1,8 @@
module NamespacesHelper
+ def namespace_id_from(params)
+ params.dig(:project, :namespace_id) || params[:namespace_id]
+ end
+
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
@@ -10,7 +14,7 @@ module NamespacesHelper
data_attr_users = { 'data-options-parent' => 'users' }
group_opts = [
- "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] }
+ "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] }
]
users_opts = [
@@ -29,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 e21178c7377..c1523b4dabf 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,10 +1,4 @@
module NavHelper
- def page_sidebar_class
- if pinned_nav?
- "page-sidebar-expanded page-sidebar-pinned"
- end
- end
-
def page_gutter_class
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
@@ -32,10 +26,6 @@ module NavHelper
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
- if pinned_nav?
- class_name << " header-sidebar-expanded header-sidebar-pinned"
- end
-
class_name
end
@@ -46,8 +36,4 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
-
- def pinned_nav?
- cookies[:pin_nav] == 'true'
- end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 7d4d049101a..3286a92a8a7 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -34,6 +34,10 @@ module PageLayoutHelper
end
end
+ def favicon
+ Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
+ end
+
def page_image
default = image_url('gitlab_logo.png')
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 6e68aad4cb7..243ef39ef61 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -23,7 +23,7 @@ module PreferencesHelper
if defined.size != DASHBOARD_CHOICES.size
# Ensure that anyone adding new options updates this method too
- raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
+ raise "`User` defines #{defined.size} dashboard choices," \
" but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
else
defined.map do |key, _|
@@ -35,16 +35,11 @@ module PreferencesHelper
def project_view_choices
[
- ['Readme (default)', :readme],
- ['Activity view', :activity],
- ['Files view', :files]
+ ['Files and Readme (default)', :files],
+ ['Activity', :activity]
]
end
- def user_application_theme
- Gitlab::Themes.for_user(current_user).css_class
- end
-
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb98204285d..4befeacc135 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -150,6 +150,15 @@ 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
+
private
def repo_children_classes(field)
@@ -232,7 +241,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/search_helper.rb b/app/helpers/search_helper.rb
index 37b69423c97..8ff8db16514 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -56,7 +56,7 @@ module SearchHelper
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
- { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
]
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index ff787fb4131..18734f1411f 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -30,7 +30,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
@@ -50,7 +50,7 @@ module SortingHelper
end
def sort_title_priority
- 'Priority'
+ 'Label priority'
end
def sort_title_oldest_updated
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index b3f50ceebe4..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)
@@ -63,7 +63,7 @@ module SubmoduleHelper
namespace = components.pop.gsub(/^\.\.$/, '')
if namespace.empty?
- namespace = @project.namespace.path
+ namespace = @project.namespace.full_path
end
[
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 c568cca9e5e..7f8efb0a4ac 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -3,6 +3,10 @@ module TodosHelper
@todos_pending_count ||= current_user.todos_pending_count
end
+ def todos_count_format(count)
+ count > 99 ? '99+' : count
+ end
+
def todos_done_count
@todos_done_count ||= current_user.todos_done_count
end
@@ -15,6 +19,7 @@ module TodosHelper
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
+ when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
end
end
@@ -86,7 +91,10 @@ module TodosHelper
[
{ id: '', text: 'Any Action' },
{ id: Todo::ASSIGNED, text: 'Assigned' },
- { id: Todo::MENTIONED, text: 'Mentioned' }
+ { id: Todo::MENTIONED, text: 'Mentioned' },
+ { id: Todo::MARKED, text: 'Added' },
+ { id: Todo::BUILD_FAILED, text: 'Pipelines' },
+ { id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
]
end
@@ -142,6 +150,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/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
new file mode 100644
index 00000000000..3e3f6246fc5
--- /dev/null
+++ b/app/helpers/wiki_helper.rb
@@ -0,0 +1,13 @@
+module WikiHelper
+ # Produces a pure text breadcrumb for a given page.
+ #
+ # page_slug - The slug of a WikiPage object.
+ #
+ # Returns a String composed of the capitalized name of each directory and the
+ # capitalized name of the page itself.
+ def breadcrumb(page_slug)
+ page_slug.split('/').
+ map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }.
+ join(' / ')
+ 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 0cd3456b4de..5b9226a6b81 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -151,7 +151,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
- headers[:subject].prepend('Re: ') if headers[:subject]
+ headers[:subject]&.prepend('Re: ')
mail_thread(model, headers)
end
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 2df8b071e13..be632930895 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,7 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_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
@@ -64,6 +64,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
@@ -76,6 +86,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 }
@@ -111,32 +127,30 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
+ validates :terminal_max_session_time,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
- unless value.nil?
- value.each do |level|
- unless Gitlab::VisibilityLevel.options.has_value?(level)
- record.errors.add(attr, "'#{level}' is not a valid visibility level")
- end
+ value&.each do |level|
+ unless Gitlab::VisibilityLevel.options.has_value?(level)
+ record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end
end
validates_each :import_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Gitlab::ImportSources.options.has_value?(source)
- record.errors.add(attr, "'#{source}' is not a import source")
- end
+ value&.each do |source|
+ unless Gitlab::ImportSources.options.has_value?(source)
+ record.errors.add(attr, "'#{source}' is not a import source")
end
end
end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
+ value&.each do |source|
+ unless Devise.omniauth_providers.include?(source.to_sym)
+ record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end
end
@@ -170,14 +184,19 @@ class ApplicationSetting < ActiveRecord::Base
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,6 +222,7 @@ 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
}
@@ -216,6 +236,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
@@ -225,11 +253,11 @@ class ApplicationSetting < ActiveRecord::Base
end
def domain_whitelist_raw
- self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
+ self.domain_whitelist&.join("\n")
end
def domain_blacklist_raw
- self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
+ self.domain_blacklist&.join("\n")
end
def domain_whitelist_raw=(values)
@@ -263,6 +291,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/award_emoji.rb b/app/models/award_emoji.rb
index 46b17479d6d..6937ad3bdd9 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ class << self
+ def votes_for_collection(ids, type)
+ select('name', 'awardable_id', 'COUNT(*) as count').
+ where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
+ group('name', 'awardable_id')
+ end
+ end
+
def downvote?
self.name == DOWNVOTE_NAME
end
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/board.rb b/app/models/board.rb
index c56422914a9..2780acc67c0 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base
validates :project, presence: true
- def backlog_list
- lists.merge(List.backlog).take
- end
-
def done_list
lists.merge(List.done).take
end
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 b1f77bf242c..3722047251d 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,7 @@ module Ci
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+ has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -19,10 +20,12 @@ module Ci
end
serialize :options
- serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
+ 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) }
@@ -41,7 +44,7 @@ module Ci
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
- before_destroy { project }
+ before_destroy { unscoped_project }
after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
@@ -52,46 +55,18 @@ 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, user = nil)
- new_build = Ci::Build.create(
- ref: build.ref,
- tag: build.tag,
- options: build.options,
- commands: build.commands,
- tag_list: build.tag_list,
- project: build.project,
- pipeline: build.pipeline,
- name: build.name,
- allow_failure: build.allow_failure,
- stage: build.stage,
- stage_idx: build.stage_idx,
- trigger_request: build.trigger_request,
- yaml_variables: build.yaml_variables,
- when: build.when,
- user: user,
- environment: build.environment,
- status_event: 'enqueue'
- )
-
- MergeRequests::AddTodoWhenBuildFailsService
- .new(build.project, nil)
- .close(new_build)
-
- build.pipeline.mark_as_processable_after_stage(build.stage_idx)
- new_build
+ def retry(build, current_user)
+ Ci::RetryBuildService
+ .new(build.project, current_user)
+ .execute(build)
end
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)
@@ -123,19 +98,24 @@ 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 play(current_user = nil)
+ def has_commands?
+ commands.present?
+ end
+
+ def play(current_user)
# Try to queue a current build
if self.enqueue
self.update(user: current_user)
@@ -151,7 +131,7 @@ module Ci
end
def retryable?
- project.builds_enabled? && commands.present? &&
+ project.builds_enabled? && has_commands? &&
(success? || failed? || canceled?)
end
@@ -183,10 +163,6 @@ module Ci
success? && !last_deployment.try(:last?)
end
- def last_deployment
- deployments.last
- end
-
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -256,11 +232,7 @@ module Ci
end
def project_id
- pipeline.project_id
- end
-
- def project_name
- project.name
+ gl_project_id
end
def repo_url
@@ -283,7 +255,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?
@@ -416,16 +388,23 @@ module Ci
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
+ # We need the project even if it's soft deleted, because whenever
+ # we're really deleting the project, we'll also delete the builds,
+ # and in order to delete the builds, we need to know where to find
+ # the artifacts, which is depending on the data of the project.
+ # We need to retain the project in this case.
+ the_project = project || unscoped_project
+
old = File.join(created_at.utc.strftime('%Y_%m'),
- project.ci_id.to_s,
+ the_project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if project.ci_id && File.directory?(old_store)
+ return old if the_project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
- project.id.to_s,
+ the_project.id.to_s,
id.to_s
)
end
@@ -451,6 +430,7 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
+ PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
@@ -504,7 +484,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
@@ -537,6 +517,27 @@ 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
@@ -559,10 +560,39 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
+ def unscoped_project
+ @unscoped_project ||= Project.unscoped.find_by(id: gl_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 },
@@ -570,14 +600,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
@@ -597,6 +625,8 @@ module Ci
end
def update_project_statistics
+ return unless project
+
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fab8497ec7d..8a5a9aa4adb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -14,9 +14,11 @@ module Ci
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?
@@ -47,6 +49,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 +99,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)
@@ -135,7 +144,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 +159,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)
@@ -214,21 +219,17 @@ module Ci
def cancel_running
Gitlab::OptimisticLocking.retry_lock(
statuses.cancelable) do |cancelable|
- cancelable.each(&:cancel)
+ cancelable.find_each(&:cancel)
end
end
- def retry_failed(user)
- Gitlab::OptimisticLocking.retry_lock(
- builds.latest.failed_or_canceled) do |failed_or_canceled|
- failed_or_canceled.select(&:retryable?).each do |build|
- Ci::Build.retry(build, user)
- end
- end
+ def retry_failed(current_user)
+ Ci::RetryPipelineService.new(project, current_user)
+ .execute(self)
end
def mark_as_processable_after_stage(stage_idx)
- builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ builds.skipped.after_stage(stage_idx).find_each(&:process)
end
def latest?
@@ -283,13 +284,7 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file ||= begin
- blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
- blob.load_all_data!(project.repository)
- blob.data
- rescue
- nil
- end
+ @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
end
def has_yaml_errors?
@@ -330,6 +325,7 @@ module Ci
when 'failed' then drop
when 'canceled' then cancel
when 'skipped' then skip
+ when 'manual' then block
end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ed1843ba005..edd21f984c8 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,8 +4,8 @@ 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
@@ -22,8 +22,6 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
- after_save :tick_runner_queue, if: :form_editable_changed?
-
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)
@@ -40,6 +38,8 @@ module Ci
acts_as_taggable
+ after_destroy :cleanup_runner_queue
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -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)
@@ -147,14 +144,14 @@ module Ci
private
- def runner_queue_key
- "runner:build_queue:#{self.token}"
+ def cleanup_runner_queue
+ Gitlab::Redis.with do |redis|
+ redis.del(runner_queue_key)
+ end
end
- def form_editable_changed?
- FORM_EDITABLE.any? do |editable|
- public_send("#{editable}_changed?")
- end
+ def runner_queue_key
+ "runner:build_queue:#{self.token}"
end
def tag_constraints
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 1f9baeca5b1..234376a7e4c 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -5,6 +5,6 @@ module Ci
belongs_to :runner
belongs_to :project, foreign_key: :gl_project_id
- validates_uniqueness_of :runner_id, scope: :gl_project_id
+ validates :runner_id, uniqueness: { scope: :gl_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..90473d41c04 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -5,10 +5,11 @@ module Ci
acts_as_paranoid
belongs_to :project, foreign_key: :gl_project_id
+ 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/commit.rb b/app/models/commit.rb
index 46f06733da1..0a18986ef26 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
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 9547c57b2ae..7e23e14794f 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -10,10 +10,11 @@ class CommitStatus < ActiveRecord::Base
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
@@ -23,29 +24,31 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
- scope :retried, -> { where.not(id: latest) }
- scope :ordered, -> { order(:name) }
-
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
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) }
+ scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :after_stage, -> (index) { where('stage_idx > ?', index) }
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
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..5101cc7e687 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,8 @@ 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'
ELSE 'failed'
END)"
end
@@ -63,6 +63,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 +74,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 +96,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 3517969eabc..3cf4c67d7e7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -15,6 +15,11 @@ module Issuable
include Taskable
include TimeTrackable
+ # This object is used to gather issuable meta data for displaying
+ # 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, :merge_requests_count)
+
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -41,12 +46,24 @@ module Issuable
has_one :metrics
+ delegate :name,
+ :email,
+ to: :author,
+ prefix: true
+
+ delegate :name,
+ :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) }
@@ -63,21 +80,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
@@ -95,8 +101,8 @@ module Issuable
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
- previous_assignee.update_cache_counts if previous_assignee
- assignee.update_cache_counts if assignee
+ previous_assignee&.update_cache_counts
+ assignee&.update_cache_counts
end
# We want to use optimistic lock for cases when only title or description are involved
@@ -139,6 +145,7 @@ module Issuable
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 'position_asc' then order_position_asc
else
order_by(method)
end
@@ -177,7 +184,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]
@@ -230,7 +237,7 @@ module Issuable
# 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/mentionable.rb b/app/models/concerns/mentionable.rb
index ef2c1e5d414..7e56e371b27 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,8 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
- extractor ||= Gitlab::ReferenceExtractor.
- new(project, current_user)
+ # Use custom extractor if it's passed in the function parameters.
+ if extractor
+ @extractor = extractor
+ else
+ @extractor ||= Gitlab::ReferenceExtractor.
+ new(project, current_user)
+
+ @extractor.reset_memoized_values
+ end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
@@ -55,16 +62,20 @@ module Mentionable
skip_project_check: skip_project_check?
)
- extractor.analyze(text, options)
+ @extractor.analyze(text, options)
end
- extractor
+ @extractor
end
def mentioned_users(current_user = nil)
all_references(current_user).users
end
+ def directly_addressed_users(current_user = nil)
+ all_references(current_user).directly_addressed_users
+ end
+
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author)
refs = all_references(current_user)
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e9450dd0c26..f449229864d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -73,7 +73,7 @@ module Milestoneish
def memoize_per_user(user, method_name)
@memoized ||= {}
@memoized[method_name] ||= {}
- @memoized[method_name][user.try!(:id)] ||= yield
+ @memoized[method_name][user&.id] ||= yield
end
# override in a class that includes this module to get a faster query
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..603f2dd7e5d
--- /dev/null
+++ b/app/models/concerns/relative_positioning.rb
@@ -0,0 +1,101 @@
+module RelativePositioning
+ extend ActiveSupport::Concern
+
+ MIN_POSITION = 0
+ MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+
+ included do
+ after_save :save_positionable_neighbours
+ end
+
+ def min_relative_position
+ self.class.in_projects(project.id).minimum(:relative_position)
+ 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 || MIN_POSITION
+ 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 || MAX_POSITION
+ end
+
+ def move_between(before, after)
+ return move_after(before) unless after
+ return move_before(after) unless before
+
+ pos_before = before.relative_position
+ pos_after = after.relative_position
+
+ if pos_after && (pos_before == pos_after)
+ self.relative_position = pos_before
+ before.move_before(self)
+ after.move_after(self)
+
+ @positionable_neighbours = [before, after]
+ else
+ self.relative_position = position_between(pos_before, pos_after)
+ end
+ end
+
+ def move_before(after)
+ self.relative_position = position_between(after.prev_relative_position, after.relative_position)
+ end
+
+ def move_after(before)
+ self.relative_position = position_between(before.relative_position, before.next_relative_position)
+ end
+
+ def move_to_end
+ self.relative_position = position_between(max_relative_position, MAX_POSITION)
+ end
+
+ private
+
+ # This method takes two integer values (positions) and
+ # calculates some random position between them. The range is huge as
+ # the maximum integer value is 2147483647. Ideally, the calculated value would be
+ # exactly between those terminating values, but this will introduce possibility of a race condition
+ # so two or more issues can get the same value, we want to avoid that and we also want to avoid
+ # using a lock here. If we have two issues with distance more than one thousand, we are OK.
+ # Given the huge range of possible values that integer can fit we shoud never face a problem.
+ def position_between(pos_before, pos_after)
+ pos_before ||= MIN_POSITION
+ pos_after ||= MAX_POSITION
+
+ pos_before, pos_after = [pos_before, pos_after].sort
+
+ rand(pos_before.next..pos_after.pred)
+ 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 2b93aa30c0f..9f6d215ceb3 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -1,5 +1,5 @@
# Store object full path in separate table for easy lookup and uniq validation
-# Object must have path db field and respond to full_path and full_path_changed? methods.
+# Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable
extend ActiveSupport::Concern
@@ -9,7 +9,13 @@ module Routable
validates_associated :route
validates :route, presence: true
- before_validation :update_route_path, if: :full_path_changed?
+ scope :with_route, -> { includes(:route) }
+
+ before_validation do
+ if full_path_changed? || full_name_changed?
+ prepare_route
+ end
+ end
end
class_methods do
@@ -77,10 +83,62 @@ module Routable
end
end
+ def full_name
+ if route && route.name.present?
+ @full_name ||= route.name
+ else
+ update_route if persisted?
+
+ build_full_name
+ end
+ end
+
+ def full_path
+ if route && route.path.present?
+ @full_path ||= route.path
+ else
+ update_route if persisted?
+
+ build_full_path
+ end
+ end
+
private
- def update_route_path
+ def full_name_changed?
+ name_changed? || parent_changed?
+ end
+
+ def full_path_changed?
+ path_changed? || parent_changed?
+ end
+
+ def build_full_name
+ if parent && name
+ parent.human_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def build_full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def update_route
+ prepare_route
+ route.save
+ end
+
+ def prepare_route
route || build_route(source: self)
- route.path = full_path
+ route.path = build_full_path
+ route.name = build_full_name
+ @full_path = nil
+ @full_name = nil
end
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/spammable.rb b/app/models/concerns/spammable.rb
index 1acff093aa1..107e6764ba2 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -11,8 +11,9 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam
+ attr_accessor :spam_log
- after_validation :check_for_spam, on: :create
+ after_validation :check_for_spam, on: [:create, :update]
cattr_accessor :spammable_attrs, instance_accessor: false do
[]
@@ -21,6 +22,10 @@ module Spammable
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
end
+ def submittable_as_spam_by?(current_user)
+ current_user && current_user.admin? && submittable_as_spam?
+ end
+
def submittable_as_spam?
if user_agent_detail
user_agent_detail.submittable? && current_application_settings.akismet_enabled
@@ -34,9 +39,14 @@ module Spammable
end
def check_for_spam
- if spam?
- self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
- end
+ error_msg = if Gitlab::Recaptcha.enabled?
+ "Your #{spammable_entity_type} has been recognized as spam. "\
+ "You can still submit it by solving Captcha."
+ else
+ "Your #{spammable_entity_type} has been recognized as spam and has been discarded."
+ end
+
+ self.errors.add(:base, error_msg) if spam?
end
def spammable_entity_type
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 040e3a2884e..9cf83440784 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, as: :trackable, dependent: :destroy
+ has_many :timelogs, dependent: :destroy
end
def spend_time(options)
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/deployment.rb b/app/models/deployment.rb
index 91d85c2279b..afad001d50f 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
- def stoppable?
+ def stop_action?
stop_action.present?
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/directly_addressed_user.rb b/app/models/directly_addressed_user.rb
new file mode 100644
index 00000000000..0d519c6ac22
--- /dev/null
+++ b/app/models/directly_addressed_user.rb
@@ -0,0 +1,7 @@
+class DirectlyAddressedUser
+ class << self
+ def reference_pattern
+ User.reference_pattern
+ end
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 577367f1eed..bf33010fd21 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
- has_many :deployments
+ has_many :deployments, dependent: :destroy
+ has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
+ scope :order_by_last_deployed_at, -> do
+ max_deployment_id_sql =
+ Deployment.select(Deployment.arel_table[:id].maximum).
+ where(Deployment.arel_table[:environment_id].eq(arel_table[:id])).
+ to_sql
+ order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
+ end
state_machine :state, initial: :available do
event :start do
@@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref)
end
- def last_deployment
- deployments.last
- end
-
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
@@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+ def last_deployed_at
+ last_deployment.try(:created_at)
+ end
+
def update_merge_request_metrics?
(environment_type || name) == "production"
end
@@ -110,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
- def stoppable?
+ def stop_action?
available? && stop_action.present?
end
- def stop!(current_user)
- return unless stoppable?
+ def stop_with_action!(current_user)
+ return unless available?
- stop
- stop_action.play(current_user)
+ stop!
+ stop_action&.play(current_user)
end
def actions_for(environment)
@@ -137,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:
@@ -171,6 +187,15 @@ class Environment < ActiveRecord::Base
self.slug = slugified
end
+ def external_url_for(path, commit_sha)
+ return unless self.external_url
+
+ public_path = project.public_path_for_source_path(path, commit_sha)
+ return unless public_path
+
+ [external_url, public_path].join('/')
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/event.rb b/app/models/event.rb
index 2662f170765..d7ca8e3c599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -36,18 +36,19 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do
- where(project_id: projects.map(&:id)).recent
+ where(project_id: projects.pluck(:id)).recent
end
- scope :with_associations, -> { includes(project: :namespace) }
+ scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
- where("action = ? OR (target_type in (?) AND action in (?))",
- Event::PUSHED, ["MergeRequest", "Issue"],
- [Event::CREATED, Event::CLOSED, Event::MERGED])
+ where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
+ Event::PUSHED,
+ %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
+ "Note", Event::COMMENTED)
end
def limit_recent(limit = 20, offset = nil)
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 4cdfd022094..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
@@ -81,7 +82,7 @@ class Group < Namespace
end
def to_reference(_from_project = nil, full: nil)
- "#{self.class.reference_prefix}#{name}"
+ "#{self.class.reference_prefix}#{full_path}"
end
def web_url
@@ -93,7 +94,7 @@ class Group < Namespace
end
def visibility_level_field
- visibility_level
+ :visibility_level
end
def visibility_level_allowed_by_projects
@@ -197,14 +198,29 @@ class Group < Namespace
end
def refresh_members_authorized_projects
- UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
+ UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
+ execute
+ end
+
+ def user_ids_for_project_authorizations
+ users_with_parents.pluck(:id)
end
def members_with_parents
- GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
+ GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
end
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/group_milestone.rb b/app/models/group_milestone.rb
index 7b6db2634b7..86d38e5468b 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -9,7 +9,7 @@ class GroupMilestone < GlobalMilestone
def self.build(group, projects, title)
super(projects, title).tap do |milestone|
- milestone.group = group if milestone
+ milestone&.group = group
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..0f7a26ee3e1 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'
diff --git a/app/models/label.rb b/app/models/label.rb
index 5b6b9a7a736..f68a8c9cff2 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
diff --git a/app/models/list.rb b/app/models/list.rb
index 065d75bd1dc..1e5da7f4dd4 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, done: 2 }
+ enum list_type: { label: 1, done: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/member.rb b/app/models/member.rb
index 26a6054e00d..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],
@@ -47,6 +49,7 @@ class Member < ActiveRecord::Base
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
+ scope :non_request, -> { where(requested_at: nil) }
scope :has_access, -> { active.where('access_level > 0') }
@@ -72,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 082adcafcc8..0f7b8311588 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -91,17 +91,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 +199,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 +436,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
@@ -527,7 +527,7 @@ class MergeRequest < ActiveRecord::Base
}
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)
@@ -546,7 +546,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
- def cache_merge_request_closes_issues!(current_user = self.author)
+ def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker?
transaction do
@@ -558,14 +558,10 @@ class MergeRequest < ActiveRecord::Base
end
end
- def closes_issue?(issue)
- closes_issues.include?(issue)
- end
-
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = [description]
+ messages = [title, description]
messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
@@ -575,13 +571,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- def issues_mentioned_but_not_closing(current_user = self.author)
+ def issues_mentioned_but_not_closing(current_user)
return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user)
- ext.analyze(description)
+ ext.analyze("#{title}\n#{description}")
- ext.issues - closes_issues
+ ext.issues - closes_issues(current_user)
end
def target_project_path
@@ -602,7 +598,7 @@ class MergeRequest < ActiveRecord::Base
def source_project_namespace
if source_project && source_project.namespace
- source_project.namespace.path
+ source_project.namespace.full_path
else
"(removed)"
end
@@ -610,7 +606,7 @@ class MergeRequest < ActiveRecord::Base
def target_project_namespace
if target_project && target_project.namespace
- target_project.namespace.path
+ target_project.namespace.full_path
else
"(removed)"
end
@@ -648,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')
@@ -688,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?
@@ -710,23 +709,27 @@ 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
- def environments
+ def environments_for(current_user)
return [] unless diff_head_commit
- @environments ||= begin
- target_envs = target_project.environments_for(
- target_branch, commit: diff_head_commit, with_tags: true)
+ @environments ||= Hash.new do |h, current_user|
+ envs = EnvironmentsFinder.new(target_project, current_user,
+ ref: target_branch, commit: diff_head_commit, with_tags: true).execute
- source_envs = source_project.environments_for(
- source_branch, commit: diff_head_commit) if source_project
+ if source_project
+ envs.concat EnvironmentsFinder.new(source_project, current_user,
+ ref: source_branch, commit: diff_head_commit).execute
+ end
- (target_envs.to_a + source_envs.to_a).uniq
+ h[current_user] = envs.uniq
end
+
+ @environments[current_user]
end
def state_human_name
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index dadb81f9b6e..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
@@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base
# When compare merge request versions we want diff A..B instead of A...B
# so we handle cases when user does squash and rebase of the commits between versions.
# For this reason we set straight to true by default.
- CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
+ CompareService.new(project, head_commit_sha)
+ .execute(project, sha, straight: straight)
end
def commits_count
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/namespace.rb b/app/models/namespace.rb
index 67d8c1c2e4c..d350f1d6770 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -7,6 +7,11 @@ class Namespace < ActiveRecord::Base
include Gitlab::CurrentSettings
include Routable
+ # Prevent users from creating unreasonably deep level of nesting.
+ # The number 20 was taken based on maximum nesting level of
+ # Android repo (15) + some extra backup.
+ NUMBER_OF_ANCESTORS_ALLOWED = 20
+
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
@@ -15,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,
@@ -29,13 +35,15 @@ class Namespace < ActiveRecord::Base
length: { maximum: 255 },
namespace: true
+ validate :nesting_level_allowed
+
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
- before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
+ before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
scope :root, -> { where('type IS NULL') }
@@ -91,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
@@ -130,6 +132,7 @@ class Namespace < ActiveRecord::Base
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
remove_exports!
@@ -169,31 +172,14 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
- def full_path
- if parent
- parent.full_path + '/' + path
- else
- path
- end
- end
-
def shared_runners_enabled?
projects.with_shared_runners.any?
end
- def full_name
- @full_name ||=
- if parent
- parent.full_name + ' / ' + name
- else
- name
- end
- end
-
# Scopes the model on ancestors of the record
def ancestors
if parent_id
- path = route.path
+ path = route ? route.path : full_path
paths = []
until path.blank?
@@ -212,6 +198,22 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
+ def user_ids_for_project_authorizations
+ [owner_id]
+ end
+
+ def parent_changed?
+ parent_id_changed?
+ end
+
+ def prepare_for_destroy
+ old_repository_storage_paths
+ end
+
+ def old_repository_storage_paths
+ @old_repository_storage_paths ||= repository_storage_paths
+ end
+
private
def repository_storage_paths
@@ -225,7 +227,7 @@ class Namespace < ActiveRecord::Base
def rm_dir
# Remove the namespace directory in all storages paths used by member projects
- @old_repository_storage_paths.each do |repository_storage_path|
+ old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash.
# We will remove it later async
new_path = "#{path}+#{id}+deleted"
@@ -250,10 +252,6 @@ class Namespace < ActiveRecord::Base
find_each(&:refresh_members_authorized_projects)
end
- def full_path_changed?
- path_changed? || parent_id_changed?
- end
-
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
@@ -269,4 +267,10 @@ class Namespace < ActiveRecord::Base
path_was
end
end
+
+ def nesting_level_allowed
+ if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
+ errors.add(:parent_id, "has too deep level of nesting")
+ end
+ 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 bf090a0438c..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
@@ -108,6 +109,12 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
+
+ def count_for_collection(ids, type)
+ user.select('noteable_id', 'COUNT(*) as count').
+ group(:noteable_id).
+ where(noteable_type: type, noteable_id: ids)
+ end
end
def cross_reference?
@@ -225,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
@@ -270,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
new file mode 100644
index 00000000000..f2f2fc1e32a
--- /dev/null
+++ b/app/models/pages_domain.rb
@@ -0,0 +1,119 @@
+class PagesDomain < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, hostname: true
+ 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
+
+ validate :validate_pages_domain
+ validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
+
+ attr_encrypted :key,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ after_create :update
+ after_save :update
+ after_destroy :update
+
+ def to_param
+ domain
+ end
+
+ def url
+ return unless domain
+
+ if certificate
+ "https://#{domain}"
+ else
+ "http://#{domain}"
+ end
+ end
+
+ def has_matching_key?
+ return false unless x509
+ return false unless pkey
+
+ # We compare the public key stored in certificate with public key from certificate key
+ x509.check_private_key(pkey)
+ end
+
+ def has_intermediates?
+ return false unless x509
+
+ # self-signed certificates doesn't have the certificate chain
+ return true if x509.verify(x509.public_key)
+
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+
+ # This forces to load all intermediate certificates stored in `certificate`
+ Tempfile.open('certificate_chain') do |f|
+ f.write(certificate)
+ f.flush
+ store.add_file(f.path)
+ end
+
+ store.verify(x509)
+ rescue OpenSSL::X509::StoreError
+ false
+ end
+
+ def expired?
+ return false unless x509
+ current = Time.new
+ current < x509.not_before || x509.not_after < current
+ end
+
+ def subject
+ return unless x509
+ x509.subject.to_s
+ end
+
+ def certificate_text
+ @certificate_text ||= x509.try(:to_text)
+ end
+
+ private
+
+ def update
+ ::Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def validate_matching_key
+ unless has_matching_key?
+ self.errors.add(:key, "doesn't match the certificate")
+ end
+ end
+
+ def validate_intermediates
+ unless has_intermediates?
+ self.errors.add(:certificate, 'misses intermediates')
+ end
+ end
+
+ def validate_pages_domain
+ return unless domain
+ if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
+ self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
+ end
+ end
+
+ def x509
+ return unless certificate
+ @x509 ||= OpenSSL::X509::Certificate.new(certificate)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+
+ def pkey
+ return unless key
+ @pkey ||= OpenSSL::PKey::RSA.new(key)
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
+ nil
+ end
+end
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 37f4705adbd..8c2dadf4659 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
@@ -53,6 +53,8 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
+ after_destroy :remove_pages
+
# update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
@@ -68,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
@@ -112,6 +113,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
@@ -148,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :pages_domains, dependent: :destroy
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
@@ -169,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
@@ -188,8 +194,8 @@ 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 :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -207,10 +213,13 @@ 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) }
+ scope :with_deleted, -> { unscope(where: :pending_delete) }
+
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
@@ -225,7 +234,12 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
- scope :inside_path, ->(path) { joins(:route).where('routes.path LIKE ?', "#{path}/%") }
+ scope :inside_path, ->(path) do
+ # We need routes alias rs for JOIN so it does not conflict with
+ # includes(:route) which we use in ProjectsFinder.
+ joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'").
+ where('rs.path LIKE ?', "#{path}/%")
+ end
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
@@ -323,7 +337,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)
@@ -348,7 +362,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})\/)?
@@ -370,10 +384,6 @@ class Project < ActiveRecord::Base
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
-
- # Add alias for Routable method for compatibility with old code.
- # In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
- alias_method :find_with_namespace, :find_by_full_path
end
def lfs_enabled?
@@ -383,7 +393,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
@@ -447,13 +457,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.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}"
@@ -465,7 +476,7 @@ class Project < ActiveRecord::Base
def reset_cache_and_import_attrs
ProjectCacheWorker.perform_async(self.id)
- self.import_data.destroy if self.import_data
+ self.import_data&.destroy
end
def import_url=(value)
@@ -546,7 +557,7 @@ class Project < ActiveRecord::Base
end
def check_limit
- unless creator.can_create_project? or namespace.kind == 'group'
+ unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
if projects_limit == 0
@@ -761,6 +772,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
@@ -811,26 +830,6 @@ class Project < ActiveRecord::Base
end
end
- def name_with_namespace
- @name_with_namespace ||= begin
- if namespace
- namespace.human_name + ' / ' + name
- else
- name
- end
- end
- end
- alias_method :human_name, :name_with_namespace
-
- def full_path
- if namespace && path
- namespace.full_path + '/' + path
- else
- path
- end
- end
- alias_method :path_with_namespace, :full_path
-
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
@@ -851,10 +850,6 @@ class Project < ActiveRecord::Base
false
end
- def empty_repo?
- repository.empty_repo?
- end
-
def repo
repository.raw
end
@@ -863,10 +858,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
@@ -889,8 +880,14 @@ class Project < ActiveRecord::Base
url_to_repo
end
- def http_url_to_repo
- "#{web_url}.git"
+ def http_url_to_repo(user = nil)
+ url = web_url
+
+ if user
+ url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
+ end
+
+ "#{url}.git"
end
# Check if current branch name is marked as protected in the system
@@ -915,8 +912,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}"
@@ -958,7 +955,8 @@ class Project < ActiveRecord::Base
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
- Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
+ Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.full_path)
+ Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.full_path)
end
# Expires various caches before a project is renamed.
@@ -1016,7 +1014,7 @@ class Project < ActiveRecord::Base
end
def visibility_level_field
- visibility_level
+ :visibility_level
end
def archive!
@@ -1041,10 +1039,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
@@ -1160,6 +1154,51 @@ class Project < ActiveRecord::Base
ensure_runners_token!
end
+ def pages_deployed?
+ Dir.exist?(public_pages_path)
+ end
+
+ def pages_url
+ subdomain, _, url_path = full_path.partition('/')
+
+ # The hostname always needs to be in downcased
+ # All web servers convert hostname to lowercase
+ host = "#{subdomain}.#{Settings.pages.host}".downcase
+
+ # The host in URL always needs to be downcased
+ url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+ "#{prefix}#{subdomain}."
+ end.downcase
+
+ # If the project path is the same as host, we serve it as group page
+ return url if host == url_path
+
+ "#{url}/#{url_path}"
+ end
+
+ def pages_subdomain
+ full_path.partition('/').first
+ end
+
+ def pages_path
+ File.join(Settings.pages.path, path_with_namespace)
+ end
+
+ def public_pages_path
+ File.join(pages_path, 'public')
+ end
+
+ def remove_pages
+ # 1. We rename pages to temporary directory
+ # 2. We wait 5 minutes, due to NFS caching
+ # 3. We asynchronously remove pages with force
+ temp_path = "#{path}.#{SecureRandom.hex}.deleted"
+
+ if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
+ PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
+ end
+ end
+
def wiki
@wiki ||= ProjectWiki.new(self, self.owner)
end
@@ -1206,7 +1245,7 @@ class Project < ActiveRecord::Base
end
def ensure_dir_exist
- gitlab_shell.add_namespace(repository_storage_path, namespace.path)
+ gitlab_shell.add_namespace(repository_storage_path, namespace.full_path)
end
def predefined_variables
@@ -1214,7 +1253,7 @@ class Project < ActiveRecord::Base
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: path, public: true },
{ key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: namespace.path, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: web_url, public: true }
]
end
@@ -1267,30 +1306,40 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit: nil, with_tags: false)
- deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
+ def route_map_for(commit_sha)
+ @route_maps_by_commit ||= Hash.new do |h, sha|
+ h[sha] = begin
+ data = repository.route_map_for(sha)
+ next unless data
- environment_ids = deployments
- .where(deployments_query, ref.to_s)
- .group(:environment_id)
- .select(:environment_id)
+ Gitlab::RouteMap.new(data)
+ rescue Gitlab::RouteMap::FormatError
+ nil
+ end
+ end
- environments_found = environments.available
- .where(id: environment_ids).to_a
+ @route_maps_by_commit[commit_sha]
+ end
- return environments_found unless commit
+ def public_path_for_source_path(path, commit_sha)
+ map = route_map_for(commit_sha)
+ return unless map
- environments_found.select do |environment|
- environment.includes_commit?(commit)
- end
+ map.public_path_for_source_path(path)
end
- def environments_recently_updated_on_branch(branch)
- environments_for(branch).select do |environment|
- environment.recently_updated_on_branch?(branch)
- end
+ def parent
+ namespace
+ end
+
+ def parent_changed?
+ namespace_id_changed?
end
+ alias_method :name_with_namespace, :full_name
+ alias_method :human_name, :full_name
+ alias_method :path_with_namespace, :full_path
+
private
def cross_namespace_reference?(from)
@@ -1329,10 +1378,6 @@ class Project < ActiveRecord::Base
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
- def full_path_changed?
- path_changed? || namespace_id_changed?
- end
-
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
@@ -1352,6 +1397,6 @@ class Project < ActiveRecord::Base
def pending_delete_twin
return false unless path
- Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
+ Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
end
end
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/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index a03605d01fb..86d271a3f69 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -30,5 +30,9 @@ module ChatMessage
def attachment_color
'#345'
end
+
+ def link(text, url)
+ "[#{text}](#{url})"
+ end
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
index 53e35cb21bf..c776e0a20c4 100644
--- a/app/models/project_services/chat_message/build_message.rb
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -7,7 +7,11 @@ module ChatMessage
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]
@@ -17,7 +21,11 @@ module ChatMessage
@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
@@ -35,7 +43,19 @@ module ChatMessage
private
def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ "#{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)
@@ -64,11 +84,11 @@ module ChatMessage
end
def branch_link
- "[#{ref}](#{branch_url})"
+ link(ref, branch_url)
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def commit_url
@@ -76,7 +96,7 @@ module ChatMessage
end
def commit_link
- "[#{Commit.truncate_sha(sha)}](#{commit_url})"
+ 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 14fd64e5332..791e5b0cec7 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -51,15 +51,16 @@ module ChatMessage
title: issue_title,
title_link: issue_url,
text: format(description),
- color: "#C95823" }]
+ color: "#C95823"
+ }]
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def issue_link
- "[#{issue_title}](#{issue_url})"
+ link(issue_title, issue_url)
end
def issue_title
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index ab5e8b24167..5e5efca7bec 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -42,7 +42,7 @@ module ChatMessage
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def merge_request_message
@@ -50,7 +50,7 @@ module ChatMessage
end
def merge_request_link
- "[merge request !#{merge_request_id}](#{merge_request_url})"
+ link("merge request !#{merge_request_id}", merge_request_url)
end
def merge_request_url
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index ca1d7207034..552113bac29 100644
--- a/app/models/project_services/chat_message/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -3,10 +3,9 @@ module ChatMessage
attr_reader :message
attr_reader :user_name
attr_reader :project_name
- attr_reader :project_link
+ attr_reader :project_url
attr_reader :note
attr_reader :note_url
- attr_reader :title
def initialize(params)
params = HashWithIndifferentAccess.new(params)
@@ -69,15 +68,15 @@ module ChatMessage
end
def description_message
- [{ text: format(@note), color: attachment_color }]
+ [{ text: format(note), color: attachment_color }]
end
def project_link
- "[#{@project_name}](#{@project_url})"
+ link(project_name, project_url)
end
def commented_on_message(target, title)
- @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
+ @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*"
end
end
end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 5eb1bd86e9d..8b5bc24fd3c 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 0a217d8caba..2717c240f05 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -12,7 +12,7 @@ class DroneCiService < CiService
def compose_service_hook
hook = service_hook || build_service_hook
# If using a service template, project may not be available
- hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
+ hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
@@ -38,8 +38,8 @@ class DroneCiService < CiService
def commit_status_path(sha, ref)
url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
+ "gitlab/#{project.full_path}/commits/#{sha}",
+ "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s
end
@@ -52,7 +52,7 @@ class DroneCiService < CiService
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
status =
- if response.code == 200 and response['status']
+ if response.code == 200 && response['status']
case response['status']
when 'killed'
:canceled
@@ -73,8 +73,8 @@ class DroneCiService < CiService
def build_page(sha, ref)
url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}"]
+ "gitlab/#{project.full_path}/redirect/commits/#{sha}",
+ "?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/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
deleted file mode 100644
index bbc312f5215..00000000000
--- a/app/models/project_services/gitlab_ci_service.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
-class GitlabCiService < CiService
- # We override the active accessor to always make GitLabCiService disabled
- # Otherwise the GitLabCiService can be picked, but should never be since it's deprecated
- def active
- false
- end
-end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 72da219df28..c4142c38b2f 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -6,7 +6,7 @@ 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
@@ -36,7 +36,7 @@ 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',
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 5d93064f9b3..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
@@ -96,7 +97,7 @@ class IrkerService < Service
rescue URI::InvalidURIError
end
- unless uri.present? and default_irc_uri.nil?
+ unless uri.present? && default_irc_uri.nil?
begin
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
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/jira_service.rb b/app/models/project_services/jira_service.rb
index 80d002f9c32..eef403dba92 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -250,21 +250,11 @@ class JiraService < IssueTrackerService
end
end
- # Build remote link on JIRA properties
- # Icons here must be available on WEB so JIRA can read the URL
- # We are using a open word graphics icon which have LGPL license
def build_remote_link_props(url:, title:, resolved: false)
status = {
resolved: resolved
}
- if resolved
- status[:icon] = {
- title: 'Closed',
- url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
- }
- end
-
{
GlobalID: 'GitLab',
object: {
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index fa3cedc4354..02fbd5497fa 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,8 +1,9 @@
class KubernetesService < DeploymentService
+ include Gitlab::CurrentSettings
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
@@ -35,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
@@ -61,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
@@ -97,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
@@ -110,7 +112,7 @@ class KubernetesService < DeploymentService
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
- map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -166,8 +168,16 @@ 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
+
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
+ end
end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index 4ebc5318da1..c13538e9fea 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,14 @@ class MattermostService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
+ { 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_builds' },
{ 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/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index b0f7a42f9a3..56f42d63b2d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
- 'Mattermost Command'
+ 'Mattermost slash commands'
end
def description
- "Perform common operations on GitLab in Mattermost"
+ "Perform common operations in Mattermost"
end
def self.to_param
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..da7496573ef 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,14 @@ class SlackService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
+ { 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_builds' },
{ 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_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index c34991e4262..2182c1c7e4b 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
- 'Slack Command'
+ 'Slack slash commands'
end
def description
- "Perform common operations on GitLab in Slack"
+ "Perform common operations in Slack"
end
def self.to_param
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 9bb456eee24..25b5d777641 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -9,8 +9,4 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
-
- def check_for_spam?
- super && project.public?
- 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 9db96347322..539b31780b3 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
@@ -60,10 +63,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
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 d77b7692d75..6ab04440ca8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -5,7 +5,8 @@ class Repository
attr_accessor :path_with_namespace, :project
- class CommitError < StandardError; end
+ 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
@@ -64,10 +61,6 @@ class Repository
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
end
- def update_autocrlf_option
- raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
- end
-
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -113,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
}
@@ -168,63 +159,46 @@ class Repository
tags.find { |tag| tag.name == name }
end
- def add_branch(user, branch_name, target)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- target = commit(target).try(:id)
+ def add_branch(user, branch_name, ref)
+ newrev = commit(ref).try(:sha)
- return false unless target
+ return false unless newrev
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- update_ref!(ref, target, oldrev)
- end
+ GitOperationService.new(user, self).add_branch(branch_name, newrev)
after_create_branch
find_branch(branch_name)
end
def add_tag(user, tag_name, target, message = nil)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
- target = commit(target).try(:id)
-
- return false unless target
-
+ newrev = commit(target).try(:id)
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service|
- raw_tag = rugged.tags.create(tag_name, target, options)
- service.newrev = raw_tag.target_id
- end
+ return false unless newrev
+
+ GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end
def rm_branch(user, branch_name)
before_remove_branch
-
branch = find_branch(branch_name)
- oldrev = branch.try(:dereferenced_target).try(:id)
- newrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
- end
+ GitOperationService.new(user, self).rm_branch(branch)
after_remove_branch
true
end
- def rm_tag(tag_name)
+ def rm_tag(user, tag_name)
before_remove_tag
+ tag = find_tag(tag_name)
- begin
- rugged.tags.delete(tag_name)
- true
- rescue Rugged::ReferenceError
- false
- end
+ GitOperationService.new(user, self).rm_tag(tag)
+
+ after_remove_tag
+ true
end
def ref_names
@@ -241,21 +215,6 @@ class Repository
false
end
- def update_ref!(name, newrev, oldrev)
- # We use 'git update-ref' because libgit2/rugged currently does not
- # offer 'compare and swap' ref updates. Without compare-and-swap we can
- # (and have!) accidentally reset the ref to an earlier state, clobbering
- # commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
- _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
- stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
- end
-
- return if status.zero?
-
- raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
- end
-
# Makes sure a commit is kept around when Git garbage collection runs.
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
@@ -353,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
@@ -435,6 +396,11 @@ class Repository
repository_event(:remove_tag)
end
+ # Runs code after removing a tag.
+ def after_remove_tag
+ expire_tags_cache
+ end
+
def before_import
expire_content_cache
end
@@ -495,6 +461,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
end
def blob_by_oid(oid)
@@ -516,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.
@@ -532,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
@@ -779,125 +753,63 @@ class Repository
@tags ||= raw_repository.tags
end
- def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+ def create_dir(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create_dir, file_path: path }]
- raw_repository.mkdir(path, options)
- end
+ multi_action(**options)
end
- def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: update
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+ def create_file(user, path, content, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create, file_path: path, content: content }]
- Gitlab::Git::Blob.commit(raw_repository, options)
- end
+ multi_action(**options)
end
- def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: true
- }
- }
+ def update_file(user, path, content, **options)
+ previous_path = options.delete(:previous_path)
+ action = previous_path && previous_path != path ? :move : :update
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+ options[:user] = user
+ options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
- if previous_path && previous_path != path
- options[:file][:previous_path] = previous_path
- Gitlab::Git::Blob.rename(raw_repository, options)
- else
- Gitlab::Git::Blob.commit(raw_repository, options)
- end
- end
+ multi_action(**options)
end
- def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- path: path
- }
- }
+ def delete_file(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :delete, file_path: path }]
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- Gitlab::Git::Blob.remove(raw_repository, options)
- end
+ multi_action(**options)
end
- def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- index = rugged.index
- parents = []
- branch = find_branch(ref)
+ # 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)
- if branch
- last_commit = branch.dereferenced_target
- index.read_tree(last_commit.raw_commit.tree)
- parents = [last_commit.sha]
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
+
+ index = Gitlab::Git::Index.new(raw_repository)
+
+ if start_commit
+ index.read_tree(start_commit.raw_commit.tree)
+ parents = [start_commit.sha]
+ else
+ parents = []
end
- actions.each do |action|
- case action[:action]
- when :create, :update, :move
- mode =
- case action[:action]
- when :update
- index.get(action[:file_path])[:mode]
- when :move
- index.get(action[:previous_path])[:mode]
- end
- mode ||= 0o100644
-
- index.remove(action[:previous_path]) if action[:action] == :move
-
- content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
- oid = rugged.write(content, :blob)
-
- index.add(path: action[:file_path], oid: oid, mode: mode)
- when :delete
- index.remove(action[:file_path])
- 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
}
@@ -906,10 +818,11 @@ class Repository
Rugged::Commit.create(rugged, options)
end
end
+ # rubocop:enable Metrics/ParameterLists
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,
@@ -918,7 +831,7 @@ class Repository
end
def user_to_committer(user)
- Gitlab::Git::committer_hash(email: user.email, name: user.name)
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
def can_be_merged?(source_sha, target_branch)
@@ -932,17 +845,18 @@ class Repository
end
end
- def merge(user, merge_request, options = {})
- our_commit = rugged.branches[merge_request.target_branch].target
- their_commit = rugged.lookup(merge_request.diff_head_sha)
+ def merge(user, source, merge_request, options = {})
+ GitOperationService.new(user, self).with_branch(
+ merge_request.target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source
- raise "Invalid merge target" if our_commit.nil?
- raise "Invalid merge source" if their_commit.nil?
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
- merge_index = rugged.merge_commits(our_commit, their_commit)
- return false if merge_index.conflicts?
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
- update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
@@ -952,34 +866,50 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
+ rescue Repository::CommitError # when merge_index.conflicts?
+ false
end
- def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- revert_tree_id ||= check_revert_content(commit, base_branch)
+ def revert(
+ user, commit, branch_name,
+ 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|
- return false unless revert_tree_id
+ revert_tree_id = check_revert_content(commit, start_commit.sha)
+ unless revert_tree_id
+ raise Repository::CreateTreeError.new('Failed to revert commit')
+ end
- update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
+ def cherry_pick(
+ user, commit, branch_name,
+ 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|
- return false unless cherry_pick_tree_id
+ 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
- update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
@@ -988,22 +918,21 @@ class Repository
},
committer: committer,
tree: cherry_pick_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def resolve_conflicts(user, branch, params)
- update_branch_with_hooks(user, branch) do
+ def resolve_conflicts(user, branch_name, params)
+ GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
end
end
- def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << { mainline: 1 } if commit.merge_commit?
+ 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)
return false if revert_index.conflicts?
@@ -1014,10 +943,9 @@ class Repository
tree_id
end
- def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << 1 if commit.merge_commit?
+ 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)
return false if cherry_pick_index.conflicts?
@@ -1075,46 +1003,37 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
end
- def fetch_ref(source_path, source_ref, target_ref)
- args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
- Gitlab::Popen.popen(args, path_to_repo)
- end
+ def with_repo_branch_commit(start_repository, start_branch_name)
+ return yield(nil) if start_repository.empty_repo?
- def create_ref(ref, ref_path)
- fetch_ref(path_to_repo, ref, ref_path)
- end
-
- def update_branch_with_hooks(current_user, branch)
- update_autocrlf_option
-
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
- target_branch = find_branch(branch)
- was_empty = empty?
+ branch_name_or_sha =
+ if start_repository == self
+ start_branch_name
+ else
+ tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
- # Make commit
- newrev = yield(ref)
+ fetch_ref(
+ start_repository.path_to_repo,
+ "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
+ tmp_ref
+ )
- unless newrev
- raise CommitError.new('Failed to create commit')
- end
+ start_repository.commit(start_branch_name).sha
+ end
- if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
- oldrev = Gitlab::Git::BLANK_SHA
- else
- oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
- end
+ yield(commit(branch_name_or_sha))
- GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
+ ensure
+ rugged.references.delete(tmp_ref) if tmp_ref
+ end
- if was_empty || !target_branch
- # If repo was empty expire cache
- after_create if was_empty
- after_create_branch
- end
- end
+ def fetch_ref(source_path, source_ref, target_ref)
+ args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
+ Gitlab::Popen.popen(args, path_to_repo)
+ end
- newrev
+ def create_ref(ref, ref_path)
+ fetch_ref(path_to_repo, ref, ref_path)
end
def ls_files(ref)
@@ -1175,8 +1094,24 @@ class Repository
end
end
+ def route_map_for(sha)
+ blob_data_at(sha, '.gitlab/route-map.yml')
+ end
+
+ def gitlab_ci_yml_for(sha)
+ blob_data_at(sha, '.gitlab-ci.yml')
+ end
+
private
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!(self)
+ blob.data
+ end
+
def refs_directory_exists?
return false unless path_with_namespace
diff --git a/app/models/route.rb b/app/models/route.rb
index dd171fdb069..73574a6206b 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- after_update :rename_descendants, if: :path_changed?
+ after_update :rename_descendants
def rename_descendants
- # We update each row separately because MySQL does not have regexp_replace.
- # rubocop:disable Rails/FindEach
- Route.where('path LIKE ?', "#{path_was}/%").each do |route|
- # Note that update column skips validation and callbacks.
- # We need this to avoid recursive call of rename_descendants method
- route.update_column(:path, route.path.sub(path_was, path))
+ if path_changed? || name_changed?
+ descendants = Route.where('path LIKE ?', "#{path_was}/%")
+
+ descendants.each do |route|
+ attributes = {}
+
+ if path_changed? && route.path.present?
+ attributes[:path] = route.path.sub(path_was, path)
+ end
+
+ if name_changed? && route.name.present?
+ attributes[:name] = route.name.sub(name_was, name)
+ end
+
+ # Note that update_columns skips validation and callbacks.
+ # We need this to avoid recursive call of rename_descendants method
+ route.update_columns(attributes) unless attributes.empty?
+ end
end
- # rubocop:enable Rails/FindEach
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 043be222f3a..2f75a2e4e7f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -27,7 +27,7 @@ class Service < ActiveRecord::Base
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
- scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
+ scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
@@ -210,7 +210,7 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w[
+ service_names = %w[
asana
assembla
bamboo
@@ -232,12 +232,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/timelog.rb b/app/models/timelog.rb
index f768c4e3da5..e166cf69703 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -1,6 +1,22 @@
class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
+ validate :issuable_id_is_present
- belongs_to :trackable, polymorphic: true
+ belongs_to :issue
+ belongs_to :merge_request
belongs_to :user
+
+ def issuable
+ issue || merge_request
+ end
+
+ private
+
+ def issuable_id_is_present
+ if issue_id && merge_request_id
+ errors.add(:base, 'Only Issue ID or Merge Request ID is required')
+ elsif issuable.nil?
+ errors.add(:base, 'Issue or Merge Request ID is required')
+ end
+ end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 2adf494ce11..47789a21133 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,12 +1,13 @@
class Todo < ActiveRecord::Base
include Sortable
- ASSIGNED = 1
- MENTIONED = 2
- BUILD_FAILED = 3
- MARKED = 4
- APPROVAL_REQUIRED = 5 # This is an EE-only feature
- UNMERGEABLE = 6
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
+ MARKED = 4
+ APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
+ DIRECTLY_ADDRESSED = 7
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -14,8 +15,9 @@ class Todo < ActiveRecord::Base
BUILD_FAILED => :build_failed,
MARKED => :marked,
APPROVAL_REQUIRED => :approval_required,
- UNMERGEABLE => :unmergeable
- }
+ UNMERGEABLE => :unmergeable,
+ DIRECTLY_ADDRESSED => :directly_addressed
+ }.freeze
belongs_to :author, class_name: "User"
belongs_to :note
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 54f5388eb2c..39c1281179b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,7 +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 :theme_id, gitlab_config.default_theme
+ default_value_for :project_view, :files
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -51,7 +51,12 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id
# Profile
- has_many :keys, dependent: :destroy
+ has_many :keys, -> do
+ type = Key.arel_table[:type]
+ where(type.not_eq('DeployKey').or(type.eq(nil)))
+ end, dependent: :destroy
+ has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy
+
has_many :emails, dependent: :destroy
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
@@ -77,14 +82,11 @@ 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
has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
- has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
@@ -93,13 +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
@@ -118,7 +129,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
- before_validation :signup_domain_valid?, on: :create
+ before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -166,10 +177,20 @@ class User < ActiveRecord::Base
def blocked?
true
end
+
+ def active_for_authentication?
+ false
+ end
+
+ def inactive_message
+ "Your account has been blocked. Please contact your GitLab " \
+ "administrator if you think this is an error."
+ end
end
end
mount_uploader :avatar, AvatarUploader
+ has_many :uploads, as: :model, dependent: :destroy
# Scopes
scope :admins, -> { where(admin: true) }
@@ -303,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 if personal_access_token
+ PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
# Returns a user for the given SSH key.
@@ -320,9 +340,34 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::NAMESPACE_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
#
@@ -443,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
@@ -526,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
@@ -566,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
@@ -903,6 +948,29 @@ class User < ActiveRecord::Base
end
end
+ def access_level
+ if admin?
+ :admin
+ else
+ :regular
+ end
+ end
+
+ def access_level=(new_level)
+ new_level = new_level.to_s
+ return unless %w(admin regular).include?(new_level)
+
+ 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
@@ -970,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_directory.rb b/app/models/wiki_directory.rb
new file mode 100644
index 00000000000..9340fc2dbbe
--- /dev/null
+++ b/app/models/wiki_directory.rb
@@ -0,0 +1,18 @@
+class WikiDirectory
+ include ActiveModel::Validations
+
+ attr_accessor :slug, :pages
+
+ validates :slug, presence: true
+
+ def initialize(slug, pages = [])
+ @slug = slug
+ @pages = pages
+ end
+
+ # Relative path to the partial to be used when rendering collections
+ # of this object.
+ def to_partial_path
+ 'projects/wikis/wiki_directory'
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index c3de278f5b7..2caebb496db 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -12,6 +12,32 @@ class WikiPage
ActiveModel::Name.new(self, nil, 'wiki')
end
+ # Sorts and groups pages by directory.
+ #
+ # pages - an array of WikiPage objects.
+ #
+ # Returns an array of WikiPage and WikiDirectory objects. The entries are
+ # sorted by alphabetical order (directories and pages inside each directory).
+ # Pages at the root level come before everything.
+ def self.group_by_directory(pages)
+ return [] if pages.blank?
+
+ pages.sort_by { |page| [page.directory, page.slug] }.
+ group_by(&:directory).
+ map do |dir, pages|
+ if dir.present?
+ WikiDirectory.new(dir, pages)
+ else
+ pages
+ end
+ end.
+ flatten
+ end
+
+ def self.unhyphenize(name)
+ name.gsub(/-+/, ' ')
+ end
+
def to_key
[:slug]
end
@@ -56,7 +82,7 @@ class WikiPage
# The formatted title of this page.
def title
if @attributes[:title]
- @attributes[:title].gsub(/-+/, ' ')
+ self.class.unhyphenize(@attributes[:title])
else
""
end
@@ -69,16 +95,17 @@ class WikiPage
# The raw content of this page.
def content
- @attributes[:content] ||= if @page
- @page.text_data
- end
+ @attributes[:content] ||= @page&.text_data
+ end
+
+ # The hierarchy of the directory this page is contained in.
+ def directory
+ wiki.page_title_and_dir(slug).last
end
# The processed/formatted content of this page.
def formatted_content
- @attributes[:formatted_content] ||= if @page
- @page.formatted_data
- end
+ @attributes[:formatted_content] ||= @page&.formatted_data
end
# The markup format for the page.
@@ -174,6 +201,16 @@ class WikiPage
end
end
+ # Relative path to the partial to be used when rendering collections
+ # of this object.
+ def to_partial_path
+ 'projects/wikis/wiki_page'
+ end
+
+ def id
+ page.version.to_s
+ end
+
private
def set_attributes
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/project_policy.rb b/app/policies/project_policy.rb
index 71ef8901932..f8594e29547 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,9 @@ class ProjectPolicy < BasePolicy
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
+ can! :admin_pages
+ can! :read_pages
+ can! :update_pages
end
def public_access!
@@ -136,6 +139,7 @@ class ProjectPolicy < BasePolicy
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
+ can! :remove_pages
end
def team_member_owner_access!
@@ -214,25 +218,7 @@ class ProjectPolicy < BasePolicy
def anonymous_rules
return unless project.public?
- can! :read_project
- can! :read_board
- can! :read_list
- can! :read_wiki
- can! :read_label
- can! :read_milestone
- can! :read_project_snippet
- can! :read_project_member
- can! :read_merge_request
- can! :read_note
- can! :read_pipeline
- can! :read_commit_status
- can! :read_container_image
- can! :download_code
- can! :download_wiki_code
- can! :read_cycle_analytics
-
- # NOTE: may be overridden by IssuePolicy
- can! :read_issue
+ base_readonly_access!
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
@@ -265,4 +251,31 @@ class ProjectPolicy < BasePolicy
:"admin_#{name}"
]
end
+
+ private
+
+ # A base set of abilities for read-only users, which
+ # is then augmented as necessary for anonymous and other
+ # read-only users.
+ def base_readonly_access!
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :read_pipeline
+ can! :read_commit_status
+ can! :read_container_image
+ can! :download_code
+ can! :download_wiki_code
+ can! :read_cycle_analytics
+
+ # NOTE: may be overridden by IssuePolicy
+ can! :read_issue
+ end
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 57acccfafd9..3a96836917e 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet if @subject.public?
return unless @user
- if @user && @subject.author == @user || @user.admin?
+ if @user && (@subject.author == @user || @user.admin?)
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
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/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
index a559d0850c4..69bf693de8d 100644
--- a/app/serializers/analytics_stage_entity.rb
+++ b/app/serializers/analytics_stage_entity.rb
@@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity
include EntityDateHelper
expose :title
+ expose :legend
expose :description
expose :median, as: :value do |stage|
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/environment_entity.rb b/app/serializers/environment_entity.rb
index 5d15eb8d3d3..4c017960628 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stoppable?
+ expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 91955542f25..d0a60f134da 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -1,3 +1,55 @@
class EnvironmentSerializer < BaseSerializer
+ Item = Struct.new(:name, :size, :latest)
+
entity EnvironmentEntity
+
+ def within_folders
+ tap { @itemize = true }
+ end
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def itemized?
+ @itemize
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ if itemized?
+ itemize(resource).map do |item|
+ { name: item.name,
+ size: item.size,
+ latest: super(item.latest, opts) }
+ end
+ else
+ resource = @paginator.paginate(resource) if paginated?
+
+ super(resource, opts)
+ end
+ end
+
+ private
+
+ def itemize(resource)
+ items = resource.order('folder_name ASC')
+ .group('COALESCE(environment_type, name)')
+ .select('COALESCE(environment_type, name) AS folder_name',
+ 'COUNT(*) AS size', 'MAX(id) AS last_id')
+
+ # It makes a difference when you call `paginate` method, because
+ # although `page` is effective at the end, it calls counting methods
+ # immediately.
+ items = @paginator.paginate(items) if paginated?
+
+ environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
+
+ items.map do |item|
+ Item.new(item.folder_name, item.size, environments[item.last_id])
+ end
+ end
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 b2de6c5832e..ab2d3d5a3ec 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,41 +1,25 @@
class PipelineSerializer < BaseSerializer
- class InvalidResourceError < StandardError; end
- include API::Helpers::Pagination
- Struct.new('Pagination', :request, :response)
+ InvalidResourceError = Class.new(StandardError)
entity PipelineEntity
- def represent(resource, opts = {})
- if paginated?
- raise InvalidResourceError unless resource.respond_to?(:page)
-
- super(paginate(resource.includes(project: :namespace)), opts)
- else
- super(resource, opts)
- end
- end
-
- def paginated?
- defined?(@pagination)
- end
-
def with_pagination(request, response)
- tap { @pagination = Struct::Pagination.new(request, response) }
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
- private
-
- # Methods needed by `API::Helpers::Pagination`
- #
- def params
- @pagination.request.query_parameters
+ def paginated?
+ @paginator.present?
end
- def request
- @pagination.request
- end
+ def represent(resource, opts = {})
+ if resource.is_a?(ActiveRecord::Relation)
+ resource = resource.includes(project: :namespace)
+ end
- def header(header, value)
- @pagination.response.headers[header] = value
+ if paginated?
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
end
end
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 c00c5aebf57..db82b8f6c30 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
@@ -61,7 +61,7 @@ module Auth
end
def process_repository_access(type, name, actions)
- requested_project = Project.find_with_namespace(name)
+ requested_project = Project.find_by_full_path(name)
return unless requested_project
actions = actions.select do |action|
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 1a2bad77a02..745c2c4b681 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -1,4 +1,5 @@
class BaseService
+ include Gitlab::Allowable
include Gitlab::CurrentSettings
attr_accessor :project, :current_user, :params
@@ -7,10 +8,6 @@ class BaseService
@project, @current_user, @params = project, user, params.dup
end
- def can?(object, action, subject)
- Ability.allowed?(object, action, subject)
- end
-
def notification_service
NotificationService.new
end
@@ -31,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/create_service.rb b/app/services/boards/create_service.rb
index 9bdd7b6f0cf..f6275a63109 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -12,7 +12,6 @@ module Boards
def create_board!
board = project.boards.create
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
board
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index fd4a462c7b2..185838764c1 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,9 +3,9 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless list.movable?
- issues = with_list_label(issues) if list.movable?
- issues
+ issues = without_board_labels(issues) unless movable_list?
+ issues = with_list_label(issues) if movable_list?
+ issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
end
private
@@ -15,12 +15,17 @@ module Boards
end
def list
- @list ||= board.lists.find(params[:id])
+ return @list if defined?(@list)
+
+ @list = board.lists.find(params[:id]) if params.key?(:id)
+ end
+
+ def movable_list?
+ @movable_list ||= list.present? && list.movable?
end
def filter_params
set_default_scope
- set_default_sort
set_project
set_state
@@ -31,16 +36,12 @@ module Boards
params[:scope] = 'all'
end
- def set_default_sort
- params[:sort] = 'priority'
- end
-
def set_project
params[:project_id] = project.id
end
def set_state
- params[:state] = list.done? ? 'closed' : 'opened'
+ params[:state] = list && list.done? ? 'closed' : 'opened'
end
def board_label_ids
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..0ab9042bf24
--- /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.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/retry_build_service.rb b/app/services/ci/retry_build_service.rb
new file mode 100644
index 00000000000..89da05b72bb
--- /dev/null
+++ b/app/services/ci/retry_build_service.rb
@@ -0,0 +1,34 @@
+module Ci
+ class RetryBuildService < ::BaseService
+ 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|
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
+
+ new_build.enqueue!
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(project, current_user)
+ .close(new_build)
+ end
+ end
+
+ def reprocess(build)
+ unless can?(current_user, :update_build, build)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ attributes = CLONE_ACCESSORS.map do |attribute|
+ [attribute, build.send(attribute)]
+ end
+
+ attributes.push([:user, current_user])
+
+ project.builds.create(Hash[attributes])
+ end
+ end
+end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
new file mode 100644
index 00000000000..574561adc4c
--- /dev/null
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -0,0 +1,28 @@
+module Ci
+ class RetryPipelineService < ::BaseService
+ include Gitlab::OptimisticLocking
+
+ def execute(pipeline)
+ unless can?(current_user, :update_pipeline, pipeline)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ pipeline.builds.failed_or_canceled.find_each do |build|
+ next unless build.retryable?
+
+ Ci::RetryBuildService.new(project, current_user)
+ .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)
+
+ pipeline.process!
+ end
+ end
+end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index cf590459cb2..42c72aba7dd 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
- next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
- environment.stop!(current_user)
+ environment.stop_with_action!(current_user)
end
end
@@ -22,8 +21,8 @@ module Ci
end
def environments
- @environments ||= project
- .environments_recently_updated_on_branch(@ref)
+ @environments ||=
+ EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end
end
end
diff --git a/app/services/ci/update_runner_service.rb b/app/services/ci/update_runner_service.rb
new file mode 100644
index 00000000000..450ee7da1c9
--- /dev/null
+++ b/app/services/ci/update_runner_service.rb
@@ -0,0 +1,15 @@
+module Ci
+ class UpdateRunnerService
+ attr_reader :runner
+
+ def initialize(runner)
+ @runner = runner
+ end
+
+ def update(params)
+ runner.update(params).tap do |updated|
+ runner.tick_runner_queue if updated
+ end
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 4d410f66c55..1297a792259 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -1,15 +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
- @source_project = params[:source_project] || @project
+ @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
@@ -25,19 +26,21 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
- tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+ validate_target_branch if different_branch?
- if tree_id
- create_target_branch(into) if @create_merge_request
+ repository.public_send(
+ action,
+ current_user,
+ @commit,
+ @target_branch,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
- repository.public_send(action, current_user, @commit, into, tree_id)
- 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
@@ -50,16 +53,17 @@ module Commits
true
end
- def create_target_branch(new_branch)
- # Temporary branch exists and contains the change commit
- return success if repository.find_branch(new_branch)
-
- result = CreateBranchService.new(@project, current_user)
- .execute(new_branch, @target_branch, source_project: @source_project)
+ def validate_target_branch
+ result = ValidateNewBranchService.new(@project, current_user)
+ .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/compare_service.rb b/app/services/compare_service.rb
index 5e8fafca98c..ab4c02a97a0 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,23 +3,27 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch, straight: false)
- source_commit = source_project.commit(source_branch)
- return unless source_commit
+ attr_reader :start_project, :start_branch_name
- source_sha = source_commit.sha
+ def initialize(new_start_project, new_start_branch_name)
+ @start_project = new_start_project
+ @start_branch_name = new_start_branch_name
+ end
+ def execute(target_project, target_branch, straight: false)
# If compare with other project we need to fetch ref first
- unless target_project == source_project
- random_string = SecureRandom.hex
+ target_project.repository.with_repo_branch_commit(
+ start_project.repository,
+ start_branch_name) do |commit|
+ break unless commit
- target_project.repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{source_branch}",
- "refs/tmp/#{random_string}/head"
- )
+ compare(commit.sha, target_project, target_branch, straight)
end
+ end
+
+ private
+ def compare(source_sha, target_project, target_branch, straight)
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
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 e004a303496..b07338d500a 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,31 +1,13 @@
class CreateBranchService < BaseService
- def execute(branch_name, ref, source_project: @project)
- valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+ def execute(branch_name, ref)
+ create_master_branch if project.empty_repo?
- unless valid_branch
- return error('Branch name is invalid')
- end
-
- repository = project.repository
- existing_branch = repository.find_branch(branch_name)
-
- if existing_branch
- return error('Branch already exists')
- end
+ result = ValidateNewBranchService.new(project, current_user)
+ .execute(branch_name)
- new_branch = if source_project != @project
- repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{ref}",
- "refs/heads/#{branch_name}"
- )
+ return result if result[:status] == :error
- repository.after_create_branch
-
- repository.find_branch(branch_name)
- else
- repository.add_branch(current_user, branch_name, ref)
- end
+ new_branch = repository.add_branch(current_user, branch_name, ref)
if new_branch
success(new_branch)
@@ -39,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/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 14f5ba064ff..40286dbf3bf 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,7 +1,8 @@
class CreateSnippetService < BaseService
+ include SpamCheckService
+
def execute
- request = params.delete(:request)
- api = params.delete(:api)
+ filter_spam_check_params
snippet = if project
project.snippets.build(params)
@@ -15,10 +16,11 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
- snippet.spam = SpamService.new(snippet, request).check(api)
+
+ spam_check(snippet, current_user)
if snippet.save
- UserAgentDetailService.new(snippet, request).create
+ UserAgentDetailService.new(snippet, @request).create
end
snippet
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
deleted file mode 100644
index fe9353afeb8..00000000000
--- a/app/services/create_tag_service.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-class CreateTagService < BaseService
- def execute(tag_name, target, message, release_description = nil)
- valid_tag = Gitlab::GitRefValidator.validate(tag_name)
- return error('Tag name invalid') unless valid_tag
-
- repository = project.repository
- message.strip! if message
-
- new_tag = nil
-
- begin
- new_tag = repository.add_tag(current_user, tag_name, target, message)
- rescue Rugged::TagError
- return error("Tag #{tag_name} already exists")
- rescue GitHooksService::PreReceiveError => ex
- return error(ex.message)
- end
-
- if new_tag
- if release_description
- CreateReleaseService.new(@project, @current_user).
- execute(tag_name, release_description)
- end
-
- success.merge(tag: new_tag)
- else
- error("Target #{target} is invalid")
- end
- end
-end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
deleted file mode 100644
index a44dee14a0f..00000000000
--- a/app/services/delete_tag_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class DeleteTagService < BaseService
- def execute(tag_name)
- repository = project.repository
- tag = repository.find_tag(tag_name)
-
- unless tag
- return error('No such tag', 404)
- end
-
- if repository.rm_tag(tag_name)
- release = project.releases.find_by(tag: tag_name)
- release.destroy if release
-
- push_data = build_push_data(tag)
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :tag_push_hooks)
- project.execute_services(push_data.dup, :tag_push_hooks)
-
- success('Tag was removed')
- else
- error('Failed to remove tag')
- end
- end
-
- def error(message, return_code = 400)
- super(message).merge(return_code: return_code)
- end
-
- def success(message)
- super().merge(message: message)
- end
-
- def build_push_data(tag)
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- tag.dereferenced_target.sha,
- Gitlab::Git::BLANK_SHA,
- "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
- [])
- end
-end
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
deleted file mode 100644
index eaff88d6463..00000000000
--- a/app/services/delete_user_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-class DeleteUserService
- attr_accessor :current_user
-
- def initialize(current_user)
- @current_user = current_user
- end
-
- def execute(user, options = {})
- if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
- user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- return user
- end
-
- user.solo_owned_groups.each do |group|
- DestroyGroupService.new(group, current_user).execute
- end
-
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
- end
-
- # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- namespace = user.namespace
- user_data = user.destroy
- namespace.really_destroy!
-
- user_data
- end
-end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
deleted file mode 100644
index 2316c57bf1e..00000000000
--- a/app/services/destroy_group_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-class DestroyGroupService
- attr_accessor :group, :current_user
-
- def initialize(group, user)
- @group, @current_user = group, user
- end
-
- def async_execute
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
-
- def execute
- group.projects.each do |project|
- # Execute the destruction of the models immediately to ensure atomic cleanup.
- # Skip repository removal because we remove directory with namespace
- # that contain all these repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
- end
-
- group.children.each do |group|
- DestroyGroupService.new(group, current_user).async_execute
- end
-
- group.really_destroy!
- end
-end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 9bd4bd464f7..c8a60422bf4 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,11 +1,11 @@
module Files
class BaseService < ::BaseService
- class ValidationError < StandardError; end
+ ValidationError = Class.new(StandardError)
def execute
- @source_project = params[:source_project] || @project
- @source_branch = params[:source_branch]
- @target_branch = params[:target_branch]
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
+ @target_branch = params[:target_branch]
@commit_message = params[:commit_message]
@file_path = params[:file_path]
@@ -22,10 +22,8 @@ module Files
# Validate parameters
validate
- # Create new branch if it different from source_branch
- if different_branch?
- create_target_branch
- end
+ # Create new branch if it different from start_branch
+ validate_target_branch if different_branch?
result = commit
if result
@@ -40,7 +38,7 @@ module Files
private
def different_branch?
- @source_branch != @target_branch || @source_project != @project
+ @start_branch != @target_branch || @start_project != @project
end
def file_has_changed?
@@ -60,23 +58,20 @@ module Files
raise_error("You are not allowed to push into this branch")
end
- unless project.empty_repo?
- unless @source_project.repository.branch_names.include?(@source_branch)
- raise_error('You can only create or edit files when you are on a branch')
- end
+ if !@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_names.include?(@target_branch)
- raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
- end
- 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
- def create_target_branch
- result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project)
+ def validate_target_branch
+ result = ValidateNewBranchService.new(project, current_user).
+ execute(@target_branch)
- unless result[:status] == :success
+ if result[:status] == :error
raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index e5b4d60e467..083ffdc634c 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,15 @@
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
+ repository.create_dir(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index b23576b9a28..65b5537fb68 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,12 +1,25 @@
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
+ repository.create_file(
+ current_user,
+ @file_path,
+ @file_content,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
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 ' +
@@ -24,7 +37,7 @@ module Files
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
- blob = repository.blob_at_branch(@source_branch, @file_path)
+ blob = repository.blob_at_branch(@start_branch, @file_path)
if blob
raise_error('Your changes could not be committed because a file with the same name already exists')
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
deleted file mode 100644
index 4f7e7a5baaa..00000000000
--- a/app/services/files/delete_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Files
- class DeleteService < Files::BaseService
- def commit
- repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
- end
- end
-end
diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb
new file mode 100644
index 00000000000..e294659bc98
--- /dev/null
+++ b/app/services/files/destroy_service.rb
@@ -0,0 +1,15 @@
+module Files
+ class DestroyService < Files::BaseService
+ def commit
+ repository.delete_file(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
+ end
+ end
+end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 54446e90007..700f9f4f6f0 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,15 +1,19 @@
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(
user: current_user,
- branch: @target_branch,
message: @commit_message,
+ branch_name: @target_branch,
actions: params[:actions],
author_email: @author_email,
- author_name: @author_name
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch
)
end
@@ -19,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]
@@ -41,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
@@ -53,7 +64,7 @@ module Files
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
- blob = repository.blob_at_branch(params[:branch_name], file_path)
+ blob = repository.blob_at_branch(params[:branch], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
@@ -61,7 +72,7 @@ module Files
end
def last_commit
- Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path)
end
def regex_check(file)
@@ -87,9 +98,23 @@ module Files
def validate_create(action)
return if project.empty_repo?
- if repository.blob_at_branch(params[:branch_name], action[:file_path])
+ 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)
@@ -100,23 +125,17 @@ module Files
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
- blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+ blob = repository.blob_at_branch(params[:branch], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
- blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+ blob = repository.blob_at_branch(params[:branch], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
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 47a18e3e132..fbbab97632e 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,14 +1,16 @@
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,
- branch: @target_branch,
- previous_path: @previous_path,
message: @commit_message,
+ branch_name: @target_branch,
+ previous_path: @previous_path,
author_email: @author_email,
- author_name: @author_name)
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
private
@@ -16,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
@@ -23,7 +29,7 @@ module Files
def last_commit
@last_commit ||= Gitlab::Git::Commit.
- last_for_path(@source_project.repository, @source_branch, @file_path)
+ last_for_path(@start_project.repository, @start_branch, @file_path)
end
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 6cd3908d43a..d222d1e63aa 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -18,9 +18,9 @@ class GitHooksService
end
end
- yield self
-
- run_hook('post-receive')
+ yield(self).tap do
+ run_hook('post-receive')
+ end
end
private
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
new file mode 100644
index 00000000000..ed6ea638235
--- /dev/null
+++ b/app/services/git_operation_service.rb
@@ -0,0 +1,156 @@
+class GitOperationService
+ attr_reader :user, :repository
+
+ def initialize(new_user, new_repository)
+ @user = new_user
+ @repository = new_repository
+ end
+
+ def add_branch(branch_name, newrev)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def rm_branch(branch)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
+ oldrev = branch.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def add_tag(tag_name, newrev, options = {})
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ with_hooks(ref, newrev, oldrev) do |service|
+ # We want to pass the OID of the tag object to the hooks. For an
+ # annotated tag we don't know that OID until after the tag object
+ # (raw_tag) is created in the repository. That is why we have to
+ # update the value after creating the tag object. Only the
+ # "post-receive" hook will receive the correct value in this case.
+ raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
+ service.newrev = raw_tag.target_id
+ end
+ end
+
+ def rm_tag(tag)
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
+ oldrev = tag.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev) do
+ repository.rugged.tags.delete(tag_name)
+ end
+ end
+
+ # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
+ # it would be created from `start_branch_name`.
+ # If `start_project` is passed, and the branch doesn't exist,
+ # it would try to find the commits from it instead of current repository.
+ def with_branch(
+ branch_name,
+ start_branch_name: nil,
+ start_project: repository.project,
+ &block)
+
+ 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_repository,
+ start_branch_name || branch_name,
+ &block)
+ end
+ end
+
+ private
+
+ def update_branch_with_hooks(branch_name)
+ update_autocrlf_option
+
+ was_empty = repository.empty?
+
+ # Make commit
+ newrev = yield
+
+ unless newrev
+ raise Repository::CommitError.new('Failed to create commit')
+ end
+
+ branch = repository.find_branch(branch_name)
+ oldrev = find_oldrev_from_branch(newrev, branch)
+
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ update_ref_in_hooks(ref, newrev, oldrev)
+
+ # If repo was empty expire cache
+ repository.after_create if was_empty
+ repository.after_create_branch if
+ was_empty || Gitlab::Git.blank_ref?(oldrev)
+
+ newrev
+ end
+
+ def find_oldrev_from_branch(newrev, branch)
+ return Gitlab::Git::BLANK_SHA unless branch
+
+ oldrev = branch.target
+
+ if oldrev == repository.rugged.merge_base(newrev, branch.target)
+ oldrev
+ else
+ raise Repository::CommitError.new('Branch diverged')
+ end
+ end
+
+ def update_ref_in_hooks(ref, newrev, oldrev)
+ with_hooks(ref, newrev, oldrev) do
+ update_ref(ref, newrev, oldrev)
+ end
+ end
+
+ def with_hooks(ref, newrev, oldrev)
+ GitHooksService.new.execute(
+ user,
+ repository.path_to_repo,
+ oldrev,
+ newrev,
+ ref) do |service|
+
+ yield(service)
+ end
+ end
+
+ def update_ref(ref, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (and have!) accidentally reset the ref to an earlier state, clobbering
+ # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+ command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(
+ command,
+ repository.path_to_repo) do |stdin|
+ stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ unless status.zero?
+ raise Repository::CommitError.new(
+ "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
+ " Please refresh and try again.")
+ end
+ end
+
+ def update_autocrlf_option
+ if repository.raw_repository.autocrlf != :input
+ repository.raw_repository.autocrlf = :input
+ 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
new file mode 100644
index 00000000000..497fdb09cdc
--- /dev/null
+++ b/app/services/groups/destroy_service.rb
@@ -0,0 +1,28 @@
+module Groups
+ class DestroyService < Groups::BaseService
+ def async_execute
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
+ end
+
+ def execute
+ group.prepare_for_destroy
+
+ group.projects.with_deleted.each do |project|
+ # Execute the destruction of the models immediately to ensure atomic cleanup.
+ # Skip repository removal because we remove directory with namespace
+ # that contain all these repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ end
+
+ group.children.each do |group|
+ # This needs to be synchronous since the namespace gets destroyed below
+ DestroyService.new(group, current_user).execute
+ end
+
+ group.really_destroy!
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 5f3ced49665..b071a398481 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -191,20 +191,19 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
- def after_update(issuable)
+ def before_update(issuable)
# To be overridden by subclasses
end
- def update_issuable(issuable, attributes)
- issuable.with_transaction_returning_status do
- issuable.update(attributes.merge(updated_by: current_user))
- end
+ def after_update(issuable)
+ # To be overridden by subclasses
end
def update(issuable)
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
@@ -212,16 +211,22 @@ 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? && update_issuable(issuable, params)
- # We do not touch as it will affect a update on updated_at field
- ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
- end
+ if issuable.changed? || params.present?
+ issuable.assign_attributes(params.merge(updated_by: current_user))
- handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
- after_update(issuable)
- issuable.create_new_cross_references!(current_user)
- execute_hooks(issuable, 'update')
+ before_update(issuable)
+
+ if issuable.with_transaction_returning_status { issuable.save }
+ # We do not touch as it will affect a update on updated_at field
+ ActiveRecord::Base.no_touching do
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ end
+
+ handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
+ issuable.create_new_cross_references!(current_user)
+ execute_hooks(issuable, 'update')
+ end
end
issuable
@@ -259,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 a63982f60c8..77bced4bd5c 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -1,50 +1,64 @@
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(params.slice(:title, :description))
+ @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
+ end
+
+ def whitelisted_issue_params
+ if can?(current_user, :admin_issue, project)
+ params.slice(:title, :description, :milestone_id)
+ else
+ params.slice(:title, :description)
+ end
end
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index d2eb46ac41b..3cf4b82b9f2 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,17 +1,20 @@
module Issues
class CreateService < Issues::BaseService
+ include SpamCheckService
+ include ResolveDiscussions
+
def execute
- @request = params.delete(:request)
- @api = params.delete(:api)
+ @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(issuable)
- issuable.spam = spam_service.check(@api)
+ def before_create(issue)
+ spam_check(issue, current_user)
+ issue.move_to_end
end
def after_create(issuable)
@@ -19,25 +22,20 @@ 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
- def spam_service
- SpamService.new(@issue, @request)
- end
-
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end
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 78cbf94ec69..a444c78b609 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -1,9 +1,17 @@
module Issues
class UpdateService < Issues::BaseService
+ include SpamCheckService
+
def execute(issue)
+ handle_move_between_iids(issue)
+ filter_spam_check_params
update(issue)
end
+ def before_update(issue)
+ spam_check(issue, current_user)
+ end
+
def handle_changes(issue, old_labels: [], old_mentioned_users: [])
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
@@ -29,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
@@ -47,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/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 12a8415d9a5..727768b1a39 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -18,5 +18,11 @@ module MergeRequests
todo_service.merge_request_build_retried(merge_request)
end
end
+
+ def close_all(pipeline)
+ pipeline_merge_requests(pipeline) do |merge_request|
+ todo_service.merge_request_build_retried(merge_request)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 1d6d2754559..9d4739e37bb 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -2,18 +2,14 @@ module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
self.merge_request = MergeRequest.new(params)
- merge_request.can_be_created = true
merge_request.compare_commits = []
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?
- if branches_specified? && branches_valid?
- compare_branches
- assign_title_and_description
- else
- merge_request.can_be_created = false
- end
+ compare_branches if branches_present?
+ assign_title_and_description if merge_request.can_be_created
merge_request
end
@@ -37,25 +33,34 @@ module MergeRequests
target_branch || target_project.default_branch
end
- def branches_specified?
- params[:source_branch] && params[:target_branch]
+ def source_branch_specified?
+ params[:source_branch].present?
+ end
+
+ def target_branch_specified?
+ params[:target_branch].present?
end
def branches_valid?
+ return false unless source_branch_specified? || target_branch_specified?
+
validate_branches
errors.blank?
end
def compare_branches
- compare = CompareService.new.execute(
+ compare = CompareService.new(
source_project,
- source_branch,
+ source_branch
+ ).execute(
target_project,
target_branch
)
- merge_request.compare_commits = compare.commits
- merge_request.compare = compare
+ if compare
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
+ end
end
def validate_branches
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index ab9056a3250..fac3ac7a4c7 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,21 +6,31 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
- attr_reader :merge_request
+ MergeError = Class.new(StandardError)
+
+ attr_reader :merge_request, :source
def execute(merge_request)
@merge_request = merge_request
- return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
+ unless @merge_request.mergeable?
+ return log_merge_error('Merge request is not mergeable', save_message_on_model: true)
+ end
+
+ @source = find_merge_source
+
+ unless @source
+ return log_merge_error('No source for merge', save_message_on_model: true)
+ end
merge_request.in_locked_state do
if commit
after_merge
success
- else
- log_merge_error('Can not merge changes', true)
end
end
+ rescue MergeError => e
+ log_merge_error(e.message, save_message_on_model: true)
end
private
@@ -34,21 +44,15 @@ module MergeRequests
committer: committer
}
- commit_id = repository.merge(current_user, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, options)
- if commit_id
- merge_request.update(merge_commit_sha: commit_id)
- else
- merge_request.update(merge_error: 'Conflicts detected during merge')
- false
- end
+ raise MergeError, 'Conflicts detected during merge' unless commit_id
+
+ merge_request.update(merge_commit_sha: commit_id)
rescue GitHooksService::PreReceiveError => e
- merge_request.update(merge_error: e.message)
- 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
@@ -66,16 +70,18 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
- def log_merge_error(message, http_error = false)
+ def log_merge_error(message, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
- error(message) if http_error
+ @merge_request.update(merge_error: message) if save_message_on_model
end
def merge_request_info
- project = merge_request.project
+ merge_request.to_reference(full: true)
+ end
- "#{project.to_reference}#{merge_request.to_reference}"
+ def find_merge_source
+ merge_request.diff_head_sha
end
end
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 b4bfb0e5e8c..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
@@ -144,7 +144,11 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- wip_commit = @commits.detect(&:work_in_progress?)
+ commit_shas = merge_request.commits_sha
+
+ wip_commit = @commits.detect do |commit|
+ commit.work_in_progress? && commit_shas.include?(commit.sha)
+ end
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
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/delete_service.rb b/app/services/notes/delete_service.rb
deleted file mode 100644
index a673e8e9dde..00000000000
--- a/app/services/notes/delete_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Notes
- class DeleteService < BaseService
- def execute(note)
- note.destroy
- end
- end
-end
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
new file mode 100644
index 00000000000..b819bd17039
--- /dev/null
+++ b/app/services/notes/destroy_service.rb
@@ -0,0 +1,7 @@
+module Notes
+ class DestroyService < BaseService
+ def execute(note)
+ note.destroy
+ end
+ end
+end
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_service.rb b/app/services/notification_service.rb
index b2cc39763f3..d12692ecc90 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
@@ -217,7 +217,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
- recipients.delete(note.author)
+ recipients.delete(note.author) unless note.author.notified_of_own_activity?
recipients = recipients.uniq
notify_method = "note_#{note.to_ability_name}_email".to_sym
@@ -327,8 +327,9 @@ class NotificationService
recipients ||= build_recipients(
pipeline,
pipeline.project,
- nil, # The acting user, who won't be added to recipients
- action: pipeline.status).map(&:notification_email)
+ pipeline.user,
+ action: pipeline.status,
+ skip_current_user: false).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
@@ -464,7 +465,7 @@ class NotificationService
end
users = users.to_a.compact.uniq
- users = users.reject(&:blocked?)
+ users = users.select { |u| u.can?(:receive_notifications) }
users.reject do |user|
global_notification_setting = user.global_notification_setting
@@ -627,7 +628,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) if skip_current_user
+ recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
recipients.uniq
end
@@ -636,7 +637,7 @@ class NotificationService
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)
+ recipients.delete(current_user) unless current_user.notified_of_own_activity?
recipients.uniq
end
diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb
new file mode 100644
index 00000000000..446eeb34d3b
--- /dev/null
+++ b/app/services/pages_service.rb
@@ -0,0 +1,15 @@
+class PagesService
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def execute
+ return unless Settings.pages.enabled
+ return unless data[:build_name] == 'pages'
+ return unless data[:build_status] == 'success'
+
+ PagesWorker.perform_async(:deploy, data[:build_id])
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index c7cce0c55b9..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
@@ -97,7 +97,7 @@ module Projects
@project.team << [current_user, :master, current_user]
end
- @project.group.refresh_members_authorized_projects if @project.group
+ @project.group&.refresh_members_authorized_projects
end
def skip_wiki?
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a08c6fcd94b..a7142d5950e 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
@@ -17,8 +17,6 @@ module Projects
def execute
return false unless can?(current_user, :remove_project, project)
- project.team.truncate
-
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
@@ -30,6 +28,7 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
Project.transaction do
+ project.team.truncate
project.destroy!
unless remove_registry_tags
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_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 06252c7b625..535da706159 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
end
def uploads_saver
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cd230528743..1c5a549feb9 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?
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 96c363c8d1a..e6193fcacee 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,7 +36,7 @@ module Projects
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
+ { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 34ec575e808..da6e6acd4a7 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)
@@ -25,11 +25,12 @@ module Projects
end
def transfer(project, new_namespace)
+ old_namespace = project.namespace
+
Project.transaction do
old_path = project.path_with_namespace
- old_namespace = project.namespace
old_group = project.group
- new_path = File.join(new_namespace.try(:path) || '', project.path)
+ new_path = File.join(new_namespace.try(:full_path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
raise TransferError.new("Project with same path in target namespace already exists")
@@ -62,13 +63,19 @@ module Projects
Labels::TransferService.new(current_user, old_group, project).execute
# Move uploads
- Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path)
+
+ # Move pages
+ Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path)
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
- true
end
+
+ refresh_permissions(old_namespace, new_namespace)
+
+ true
end
def allowed_transfer?(current_user, project, namespace)
@@ -77,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
+
+ def refresh_permissions(old_namespace, new_namespace)
+ # This ensures we only schedule 1 job for every user that has access to
+ # the namespaces.
+ user_ids = old_namespace.user_ids_for_project_authorizations |
+ new_namespace.user_ids_for_project_authorizations
+
+ UserProjectAccessChangedService.new(user_ids).execute
+ end
end
end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
new file mode 100644
index 00000000000..eb4809afa85
--- /dev/null
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -0,0 +1,69 @@
+module Projects
+ class UpdatePagesConfigurationService < BaseService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ update_file(pages_config_file, pages_config.to_json)
+ reload_daemon
+ success
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def pages_config
+ {
+ domains: pages_domains_config
+ }
+ end
+
+ def pages_domains_config
+ project.pages_domains.map do |domain|
+ {
+ domain: domain.domain,
+ certificate: domain.certificate,
+ key: domain.key,
+ }
+ end
+ end
+
+ def reload_daemon
+ # GitLab Pages daemon constantly watches for modification time of `pages.path`
+ # It reloads configuration when `pages.path` is modified
+ update_file(pages_update_file, SecureRandom.hex(64))
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def pages_config_file
+ File.join(pages_path, 'config.json')
+ end
+
+ def pages_update_file
+ File.join(::Settings.pages.path, '.update')
+ end
+
+ def update_file(file, data)
+ unless data
+ FileUtils.remove(file, force: true)
+ return
+ end
+
+ temp_file = "#{file}.#{SecureRandom.hex(16)}"
+ File.open(temp_file, 'w') do |f|
+ f.write(data)
+ end
+ FileUtils.move(temp_file, file, force: true)
+ ensure
+ # In case if the updating fails
+ FileUtils.remove(temp_file, force: true)
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
new file mode 100644
index 00000000000..523b9f41916
--- /dev/null
+++ b/app/services/projects/update_pages_service.rb
@@ -0,0 +1,166 @@
+module Projects
+ class UpdatePagesService < BaseService
+ BLOCK_SIZE = 32.kilobytes
+ MAX_SIZE = 1.terabyte
+ SITE_PATH = 'public/'.freeze
+
+ attr_reader :build
+
+ def initialize(project, build)
+ @project, @build = project, build
+ end
+
+ def execute
+ # Create status notifying the deployment of pages
+ @status = create_status
+ @status.enqueue!
+ @status.run!
+
+ raise 'missing pages artifacts' unless build.artifacts_file?
+ raise 'pages are outdated' unless latest?
+
+ # Create temporary directory in which we will extract the artifacts
+ FileUtils.mkdir_p(tmp_path)
+ Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ extract_archive!(archive_path)
+
+ # Check if we did extract public directory
+ archive_public_path = File.join(archive_path, 'public')
+ raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise 'pages are outdated' unless latest?
+
+ deploy_page!(archive_public_path)
+ success
+ end
+ rescue => e
+ error(e.message)
+ ensure
+ build.erase_artifacts! unless build.has_expiring_artifacts?
+ end
+
+ private
+
+ def success
+ @status.success
+ super
+ end
+
+ def error(message, http_status = nil)
+ @status.allow_failure = !latest?
+ @status.description = message
+ @status.drop
+ super
+ end
+
+ def create_status
+ GenericCommitStatus.new(
+ project: project,
+ pipeline: build.pipeline,
+ user: build.user,
+ ref: build.ref,
+ stage: 'deploy',
+ name: 'pages:deploy'
+ )
+ end
+
+ def extract_archive!(temp_path)
+ if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
+ extract_tar_archive!(temp_path)
+ elsif artifacts.ends_with?('.zip')
+ extract_zip_archive!(temp_path)
+ else
+ raise 'unsupported artifacts format'
+ end
+ end
+
+ def extract_tar_archive!(temp_path)
+ results = Open3.pipeline(%W(gunzip -c #{artifacts}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
+
+ def extract_zip_archive!(temp_path)
+ raise 'missing artifacts metadata' unless build.artifacts_metadata?
+
+ # Calculate page size after extract
+ public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+
+ if public_entry.total_size > max_size
+ raise "artifacts for pages are too large: #{public_entry.total_size}"
+ end
+
+ # Requires UnZip at least 6.00 Info-ZIP.
+ # -n never overwrite existing files
+ # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
+ site_path = File.join(SITE_PATH, '*')
+ unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path}))
+ raise 'pages failed to extract'
+ end
+ end
+
+ def deploy_page!(archive_public_path)
+ # Do atomic move of pages
+ # Move and removal may not be atomic, but they are significantly faster then extracting and removal
+ # 1. We move deployed public to previous public path (file removal is slow)
+ # 2. We move temporary public to be deployed public
+ # 3. We remove previous public path
+ FileUtils.mkdir_p(pages_path)
+ begin
+ FileUtils.move(public_path, previous_public_path)
+ rescue
+ end
+ FileUtils.move(archive_public_path, public_path)
+ ensure
+ FileUtils.rm_r(previous_public_path, force: true)
+ end
+
+ def latest?
+ # check if sha for the ref is still the most recent one
+ # this helps in case when multiple deployments happens
+ sha == latest_sha
+ end
+
+ def blocks
+ # Calculate dd parameters: we limit the size of pages
+ 1 + max_size / BLOCK_SIZE
+ end
+
+ def max_size
+ current_application_settings.max_pages_size.megabytes || MAX_SIZE
+ end
+
+ def tmp_path
+ @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def public_path
+ @public_path ||= File.join(pages_path, 'public')
+ end
+
+ def previous_public_path
+ @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ end
+
+ def ref
+ build.ref
+ end
+
+ def artifacts
+ build.artifacts_file.path
+ end
+
+ def latest_sha
+ project.commit(build.ref).try(:sha).to_s
+ end
+
+ def sha
+ build.sha
+ end
+ end
+end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index 012e82a7704..be34d4fa9b8 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -5,7 +5,7 @@ module Projects
end
def execute
- return nil unless @file and @file.size <= max_attachment_size
+ return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
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 3566a8ba92f..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
@@ -304,6 +316,18 @@ module SlashCommands
params '@user'
command :cc
+ desc 'Defines target branch for MR'
+ params '<Local branch name>'
+ condition do
+ issuable.respond_to?(:target_branch) &&
+ (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
+ issuable.new_record?)
+ end
+ command :target_branch do |target_branch_param|
+ branch_name = target_branch_param.strip
+ @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
+ end
+
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
@@ -317,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/spam_check_service.rb b/app/services/spam_check_service.rb
new file mode 100644
index 00000000000..023e0824e85
--- /dev/null
+++ b/app/services/spam_check_service.rb
@@ -0,0 +1,24 @@
+# SpamCheckService
+#
+# Provide helper methods for checking if a given spammable object has
+# potential spam data.
+#
+# Dependencies:
+# - params with :request
+#
+module SpamCheckService
+ def filter_spam_check_params
+ @request = params.delete(:request)
+ @api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
+ end
+
+ def spam_check(spammable, user)
+ spam_service = SpamService.new(spammable, @request)
+
+ spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do
+ user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
+ end
+ end
+end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 48903291799..3e65b7d31a3 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -1,5 +1,6 @@
class SpamService
attr_accessor :spammable, :request, :options
+ attr_reader :spam_log
def initialize(spammable, request = nil)
@spammable = spammable
@@ -16,15 +17,6 @@ class SpamService
end
end
- def check(api = false)
- return false unless request && check_for_spam?
-
- return false unless akismet.is_spam?
-
- create_spam_log(api)
- true
- end
-
def mark_as_spam!
return false unless spammable.submittable_as_spam?
@@ -35,8 +27,30 @@ class SpamService
end
end
+ def when_recaptcha_verified(recaptcha_verified, api = false)
+ # In case it's a request which is already verified through recaptcha, yield
+ # block.
+ if recaptcha_verified
+ yield
+ else
+ # Otherwise, it goes to Akismet and check if it's a spam. If that's the
+ # case, it assigns spammable record as "spam" and create a SpamLog record.
+ spammable.spam = check(api)
+ spammable.spam_log = spam_log
+ end
+ end
+
private
+ def check(api)
+ return false unless request && check_for_spam?
+
+ return false unless akismet.is_spam?
+
+ create_spam_log(api)
+ true
+ end
+
def akismet
@akismet ||= AkismetService.new(
spammable_owner,
@@ -63,7 +77,7 @@ class SpamService
end
def create_spam_log(api)
- SpamLog.create(
+ @spam_log = SpamLog.create!(
{
user_id: spammable_owner_id,
title: spammable.spam_title,
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a2bfa422c9d..868fa7b3f21 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -33,9 +33,7 @@ class SystemHooksService
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
@@ -86,7 +84,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 +101,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 a11bca00687..8e02fe3741a 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -118,16 +118,18 @@ module SystemNoteService
#
# Example Note text:
#
- # "Changed estimate of this issue to 3d 5h"
+ # "removed time estimate"
+ #
+ # "changed time estimate to 3d 5h"
#
# Returns the created Note object
def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0
- "Removed time estimate on this #{noteable.human_class_name}"
+ "removed time estimate"
else
- "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}"
+ "changed time estimate to #{parsed_time}"
end
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -142,7 +144,9 @@ module SystemNoteService
#
# Example Note text:
#
- # "Added 2h 30m of time spent on this issue"
+ # "removed time spent"
+ #
+ # "added 2h 30m of time spent"
#
# Returns the created Note object
@@ -150,11 +154,11 @@ module SystemNoteService
time_spent = noteable.time_spent
if time_spent == :reset
- body = "Removed time spent on this #{noteable.human_class_name}"
+ body = "removed time spent"
else
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
- action = time_spent > 0 ? 'Added' : 'Subtracted'
- body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}"
+ action = time_spent > 0 ? 'added' : 'subtracted'
+ body = "#{action} #{parsed_time} of time spent"
end
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -183,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)
@@ -221,7 +225,7 @@ module SystemNoteService
end
def discussion_continued_in_issue(discussion, project, author, issue)
- body = "Added #{issue.to_reference} to continue this discussion"
+ body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
note_attributes[:type] = note_attributes.delete(:note_type)
@@ -260,7 +264,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "made the issue confidential"
+ # "made the issue confidential"
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
@@ -352,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)
@@ -403,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/create_service.rb b/app/services/tags/create_service.rb
new file mode 100644
index 00000000000..1756da9e519
--- /dev/null
+++ b/app/services/tags/create_service.rb
@@ -0,0 +1,32 @@
+module Tags
+ class CreateService < BaseService
+ def execute(tag_name, target, message, release_description = nil)
+ valid_tag = Gitlab::GitRefValidator.validate(tag_name)
+ return error('Tag name invalid') unless valid_tag
+
+ repository = project.repository
+ message&.strip!
+
+ new_tag = nil
+
+ begin
+ new_tag = repository.add_tag(current_user, tag_name, target, message)
+ rescue Rugged::TagError
+ return error("Tag #{tag_name} already exists")
+ rescue GitHooksService::PreReceiveError => ex
+ return error(ex.message)
+ end
+
+ if new_tag
+ if release_description
+ CreateReleaseService.new(@project, @current_user).
+ execute(tag_name, release_description)
+ end
+
+ success.merge(tag: new_tag)
+ else
+ error("Target #{target} is invalid")
+ end
+ end
+ end
+end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
new file mode 100644
index 00000000000..a368f4f5b61
--- /dev/null
+++ b/app/services/tags/destroy_service.rb
@@ -0,0 +1,46 @@
+module Tags
+ class DestroyService < BaseService
+ def execute(tag_name)
+ repository = project.repository
+ tag = repository.find_tag(tag_name)
+
+ unless tag
+ return error('No such tag', 404)
+ end
+
+ if repository.rm_tag(current_user, tag_name)
+ release = project.releases.find_by(tag: tag_name)
+ release&.destroy
+
+ push_data = build_push_data(tag)
+ EventCreateService.new.push(project, current_user, push_data)
+ project.execute_hooks(push_data.dup, :tag_push_hooks)
+ project.execute_services(push_data.dup, :tag_push_hooks)
+
+ success('Tag was removed')
+ else
+ error('Failed to remove tag')
+ end
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+
+ def error(message, return_code = 400)
+ super(message).merge(return_code: return_code)
+ end
+
+ def success(message)
+ super().merge(message: message)
+ end
+
+ def build_push_data(tag)
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ tag.dereferenced_target.sha,
+ Gitlab::Git::BLANK_SHA,
+ "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
+ [])
+ end
+ end
+end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 1bd6ce416ab..8787a1c93a9 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:
@@ -170,16 +170,20 @@ class TodoService
# When user marks some todos as done
def mark_todos_as_done(todos, current_user)
- mark_todos_as_done_by_ids(todos.select(&:id), current_user)
+ update_todos_state_by_ids(todos.select(&:id), current_user, :done)
end
def mark_todos_as_done_by_ids(ids, current_user)
- todos = current_user.todos.where(id: ids)
+ update_todos_state_by_ids(ids, current_user, :done)
+ end
- # Only return those that are not really on that state
- marked_todos = todos.where.not(state: :done).update_all(state: :done)
- current_user.update_todos_count_cache
- marked_todos
+ # When user marks some todos as pending
+ def mark_todos_as_pending(todos, current_user)
+ update_todos_state_by_ids(todos.select(&:id), current_user, :pending)
+ end
+
+ def mark_todos_as_pending_by_ids(ids, current_user)
+ update_todos_state_by_ids(ids, current_user, :pending)
end
# When user marks an issue as todo
@@ -194,6 +198,15 @@ class TodoService
private
+ 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)
+ current_user.update_todos_count_cache
+ marked_todos
+ end
+
def create_todos(users, attributes)
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
@@ -243,6 +256,12 @@ class TodoService
end
def create_mention_todos(project, target, author, note = nil)
+ # Create Todos for directly addressed users
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
+ create_todos(directly_addressed_users, attributes)
+
+ # Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
@@ -282,10 +301,18 @@ class TodoService
)
end
+ def filter_todo_users(users, project, target)
+ reject_users_without_access(users, project, target).uniq
+ end
+
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users(author)
- mentioned_users = reject_users_without_access(mentioned_users, project, target)
- mentioned_users.uniq
+ filter_todo_users(mentioned_users, project, target)
+ end
+
+ def filter_directly_addressed_users(project, target, author)
+ directly_addressed_users = target.directly_addressed_users(author)
+ filter_todo_users(directly_addressed_users, project, target)
end
def reject_users_without_access(users, project, target)
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index a6bb36821c3..358bca73aec 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -1,4 +1,6 @@
class UpdateSnippetService < BaseService
+ include SpamCheckService
+
attr_accessor :snippet
def initialize(project, user, snippet, params)
@@ -9,7 +11,7 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
-
+
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
@@ -17,6 +19,10 @@ class UpdateSnippetService < BaseService
end
end
- snippet.update_attributes(params)
+ filter_spam_check_params
+ snippet.assign_attributes(params)
+ spam_check(snippet, current_user)
+
+ snippet.save
end
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
new file mode 100644
index 00000000000..833da5bc5d1
--- /dev/null
+++ b/app/services/users/destroy_service.rb
@@ -0,0 +1,56 @@
+module Users
+ class DestroyService
+ attr_accessor :current_user
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user, options = {})
+ unless Ability.allowed?(current_user, :destroy_user, user)
+ raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
+ end
+
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
+ user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ Groups::DestroyService.new(group, current_user).execute
+ end
+
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::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
+ namespace.really_destroy!
+
+ 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..d9370bbb598 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -115,11 +115,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/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
new file mode 100644
index 00000000000..2f61be184ce
--- /dev/null
+++ b/app/services/validate_new_branch_service.rb
@@ -0,0 +1,22 @@
+require_relative 'base_service'
+
+class ValidateNewBranchService < BaseService
+ def execute(branch_name)
+ valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+
+ unless valid_branch
+ return error('Branch name is invalid')
+ end
+
+ repository = project.repository
+ existing_branch = repository.find_branch(branch_name)
+
+ if existing_branch
+ return error('Branch already exists')
+ end
+
+ success
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+end
diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb
new file mode 100644
index 00000000000..6b93fb2f6d7
--- /dev/null
+++ b/app/services/wiki_pages/destroy_service.rb
@@ -0,0 +1,11 @@
+module WikiPages
+ class DestroyService < WikiPages::BaseService
+ def execute(page)
+ if page&.delete
+ execute_hooks(page, 'delete')
+ end
+
+ page
+ end
+ end
+end
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 47bef7cd1e4..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.prepend("!") if image_or_video?
+ 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 fbaea2744a3..7635c20ab3a 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,12 +1,15 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg]
+ 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].freeze
def image?
extension_match?(IMAGE_EXT)
@@ -20,6 +23,12 @@ module UploaderHelper
image? || video?
end
+ def dangerous?
+ extension_match?(DANGEROUS_EXT)
+ end
+
+ private
+
def extension_match?(extensions)
return false unless file
@@ -33,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/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
new file mode 100644
index 00000000000..098b16017d2
--- /dev/null
+++ b/app/validators/certificate_key_validator.rb
@@ -0,0 +1,25 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate_key: true
+# end
+#
+class CertificateKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_private_key_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM private key")
+ end
+ end
+
+ private
+
+ def valid_private_key_pem?(value)
+ return false unless value
+ pkey = OpenSSL::PKey::RSA.new(value)
+ pkey.private?
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
new file mode 100644
index 00000000000..e3d18097f71
--- /dev/null
+++ b/app/validators/certificate_validator.rb
@@ -0,0 +1,24 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate: true
+# end
+#
+class CertificateValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_certificate_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM certificate")
+ end
+ end
+
+ private
+
+ def valid_certificate_pem?(value)
+ return false unless value
+ OpenSSL::X509::Certificate.new(value).present?
+ rescue OpenSSL::X509::CertificateError
+ false
+ end
+end
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/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 c4b748d0ab8..6a5e170ddd8 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -12,7 +12,8 @@
%th.wide Message
%th Action
= render @abuse_reports
+ = paginate @abuse_reports, theme: 'gitlab'
- 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/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 558bbe07b16..00366b0a8c9 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -187,6 +187,14 @@
.help-block Markdown enabled
%fieldset
+ %legend Pages
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block 0 for unlimited
+
+ %fieldset
%legend Continuous Integration
.form-group
.col-sm-offset-2.col-sm-10
@@ -204,8 +212,16 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
.help-block
- Set the maximum file size each build'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
@@ -344,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
@@ -509,5 +548,15 @@
.help-block
Number of Git pushes after which 'git gc' is run.
+ %fieldset
+ %legend Web terminal
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .help-block
+ Maximum time for web terminal websocket connection (in seconds).
+ 0 for unlimited.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 4f982a6e369..ac36bb5bb17 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -35,7 +35,7 @@
.clearfix
%p
%i.fa.fa-exclamation-circle
- If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
+ If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
%p
%i.fa.fa-exclamation-circle
If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 5e3f105d41f..66d633119c2 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -12,7 +12,7 @@
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block
- #{(@scope || 'all').capitalize} builds
+ #{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b5f96363230..7893c1dee97 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,9 +20,9 @@
%span
Groups
= nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
+ = link_to admin_builds_path, title: 'Jobs' do
%span
- Builds
+ Jobs
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 5238623e936..e67ad663720 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -163,6 +163,6 @@
- @groups.each do |group|
%p
= link_to [:admin, group], class: 'str-truncated-60' do
- = group.name
+ = group.full_name
%span.light.pull-right
#{time_ago_with_tooltip(group.created_at)}
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/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 0a954c20fcd..5e585ce789b 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,17 +8,16 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
+ %li{ class: active_when(klass == Gitlab::GitLogger) }>
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
- .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
- id: klass::file_name_noext }
+ .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
.file-holder#README
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file
= klass::file_name
.pull-right
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 2e6f03fcde0..3301f55b8a8 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,38 +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 = 'Search for Namespace'
- - if params[:namespace_id].present?
- - namespace = Namespace.find(params[:namespace_id])
- - toggle_text = "#{namespace.kind}: #{namespace.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
-
- = button_tag "Search", class: "btn btn-primary btn-search"
+ .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 }
@@ -46,50 +32,14 @@
= link_to admin_projects_path do
All
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
- .nav-controls
- = render 'shared/projects/dropdown'
- = link_to new_project_path, class: 'btn btn-new' do
- New Project
-
- .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 975bd950ae1..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
@@ -22,7 +24,7 @@
%td
#{runner.builds.count(:all)}
%td
- - runner.tag_list.each do |tag|
+ - runner.tag_list.sort.each do |tag|
%span.label.label-primary
= tag
%td
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 124f970524e..7d26864d0f3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -26,7 +26,7 @@
.bs-callout
%p
- A 'Runner' is a process which runs a build.
+ A 'Runner' is a process which runs a job.
You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, even on your local machine.
@@ -37,16 +37,16 @@
%ul
%li
%span.label.label-success shared
- \- Runner runs builds from all unassigned projects
+ \- Runner runs jobs from all unassigned projects
%li
%span.label.label-info specific
- \- Runner runs builds from assigned projects
+ \- Runner runs jobs from assigned projects
%li
%span.label.label-warning locked
\- Runner cannot be assigned to other projects
%li
%span.label.label-danger paused
- \- Runner will not receive any new builds
+ \- Runner will not receive any new jobs
.append-bottom-20.clearfix
.pull-left
@@ -56,7 +56,7 @@
= submit_tag 'Search', class: 'btn'
.pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ Runners with last contact more than a minute ago: #{@active_runners_cnt}
%br
@@ -67,8 +67,9 @@
%th Type
%th Runner token
%th Description
+ %th Version
%th Projects
- %th Builds
+ %th Jobs
%th Tags
%th Last contact
%th
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 39e103e3062..dc4116e1ce0 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,13 +11,13 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This Runner will process builds from ALL UNASSIGNED projects
+ %h4 This Runner will process jobs from ALL UNASSIGNED projects
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This Runner will process builds only from ASSIGNED projects
+ %h4 This Runner will process jobs only from ASSIGNED projects
%p You can't make this a shared Runner.
%hr
@@ -70,11 +70,11 @@
= paginate @projects, theme: "gitlab"
.col-md-6
- %h4 Recent builds served by this Runner
+ %h4 Recent jobs served by this Runner
%table.table.ci-table.runner-builds
%thead
%tr
- %th Build
+ %th Job
%th Status
%th Project
%th Commit
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 4ce4eab8753..33f6d847782 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -14,6 +14,8 @@
%td
= spam_log.via_api? ? 'Y' : 'N'
%td
+ = spam_log.recaptcha_verified ? 'Y' : 'N'
+ %td
= spam_log.noteable_type
%td
= spam_log.title
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 0fdd5bd9960..8aaa6379730 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -10,6 +10,7 @@
%th User
%th Source IP
%th API?
+ %th Recaptcha verified?
%th Type
%th Title
%th Description
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
new file mode 100644
index 00000000000..7855239dfe5
--- /dev/null
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -0,0 +1,37 @@
+%fieldset
+ %legend Access
+ .form-group
+ = f.label :projects_limit, class: 'control-label'
+ .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+
+ .form-group
+ = f.label :can_create_group, class: 'control-label'
+ .col-sm-10= f.check_box :can_create_group
+
+ .form-group
+ = f.label :access_level, class: 'control-label'
+ .col-sm-10
+ - editing_current_user = (current_user == @user)
+
+ = f.radio_button :access_level, :regular, disabled: editing_current_user
+ = label_tag :regular do
+ Regular
+ %p.light
+ Regular users have access to their groups and projects
+
+ = f.radio_button :access_level, :admin, disabled: editing_current_user
+ = label_tag :admin do
+ Admin
+ %p.light
+ Administrators have access to all groups, projects and users and can manage all features in this installation
+ - if editing_current_user
+ %p.light
+ You cannot remove your own admin rights.
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10
+ = f.check_box :external do
+ External
+ %p.light
+ External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3145212728f..e911af3f6f9 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -40,28 +40,7 @@
= f.label :password_confirmation, class: 'control-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
- %fieldset
- %legend Access
- .form-group
- = f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
-
- .form-group
- = f.label :can_create_group, class: 'control-label'
- .col-sm-10= f.check_box :can_create_group
-
- .form-group
- = f.label :admin, class: 'control-label'
- - if current_user == @user
- .col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights.
- - else
- .col-sm-10= f.check_box :admin
-
- .form-group
- = f.label :external, class: 'control-label'
- .col-sm-10= f.check_box :external
- .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+ = render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
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/index.html.haml b/app/views/admin/users/index.html.haml
index 4dc44225d49..298cf0fa950 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -38,31 +38,31 @@
.nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.fade-left
- = nav_link(html_options: { class: ('active' unless params[:filter]) }) do
+ = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
- = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
- = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
%small.badge= number_with_delimiter(User.external.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
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/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index b0bee1c6204..dfbc7772698 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -11,7 +11,7 @@
.form-group
.col-sm-12
.file-holder
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
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/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 02b94beee92..ecdf76ef5c5 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,8 @@
-%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }>
- = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }>
- = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
- Starred Projects
+.top-area
+ %ul.nav-links
+ %li{ class: active_when(params[:filter].nil?) }>
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
+ Your Projects
+ %li{ class: active_when(params[:filter] == 'starred') }>
+ = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
+ Starred Projects
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 605bfd0cf8d..388190642aa 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,28 +1,33 @@
%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
- = author_avatar(todo, size: 40)
+ .todo-avatar
+ = author_avatar(todo, size: 40)
.todo-item.todo-block
.todo-title.title
- unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
- %span.author-name
+ .title-item.author-name
- if todo.author
= link_to_author(todo)
- else
(removed)
- %span.action-name
+ .title-item.action-name
= todo_action_name(todo)
- %span.todo-label
+ .title-item.todo-label
- if todo.target
= todo_target_link(todo)
- else
(removed)
- &middot; #{time_ago_with_tooltip(todo.created_at)}
- = todo_due_date(todo)
+ .title-item
+ &middot;
+
+ .title-item
+ #{time_ago_with_tooltip(todo.created_at)}
+ = todo_due_date(todo)
.todo-body
.todo-note
@@ -31,6 +36,14 @@
- if todo.pending?
.todo-actions
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
Done
= icon('spinner spin')
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
+ Undo
+ = icon('spinner spin')
+ - else
+ .todo-actions
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-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 c4bf2c90cc2..d7e0a8e4b2c 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -4,15 +4,13 @@
- if current_user.todos.any?
.top-area
%ul.nav-links
- - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{ class: "todos-pending #{todo_pending_active}" }>
+ %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- - todo_done_active = ('active' if params[:state] == 'done')
- %li{ class: "todos-done #{todo_done_active}" }>
+ %li.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -48,16 +46,16 @@
= 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
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/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index e87a16a5157..f92f89e73ff 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -6,4 +6,4 @@
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+ = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn')
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index eddfce363a7..da4769e214e 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -4,7 +4,7 @@
.login-body
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) }
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 01ecf237925..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...
@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- - if current_application_settings.recaptcha_enabled
+ - if Gitlab::Recaptcha.enabled?
= recaptcha_tags
%div
= f.submit "Register", class: "btn-register btn"
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 8c4ad30c832..dd34600490e 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -3,7 +3,7 @@
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li{ class: (:active if i.zero? && !crowd_enabled?) }
+ %li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
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/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 3a95a652810..94408b92374 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -2,7 +2,7 @@
- blob = discussion.blob
.diff-file.file-holder
- .file-title
+ .js-file-title.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
.diff-content.code.js-syntax-highlight
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/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index f0b61e0f7de..e30ee1b0e05 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,6 +1,5 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
- ":discussion-id" => "'#{discussion.id}'",
+ %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
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..43a52cf3002 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -4,7 +4,7 @@ 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
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 64ca3c32e01..efd13aabf20 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -3,11 +3,9 @@
.event-title
%span.author_name= link_to_author event
%span.pushed #{event.action_name} #{event.ref_type}
- - if event.rm_ref?
- %strong= event.ref_name
- - else
- %strong
- = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title)
+ %strong
+ - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
+ = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link
= render "events/event_scope", event: event
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..8374f5a009f 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -5,41 +5,11 @@
= 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
-
+- if @groups.present?
+ = render 'groups'
+- else
+ .nothing-here-block No public groups
= paginate @groups, theme: "gitlab"
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index e3088848492..56f463572bb 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -13,7 +13,7 @@
= link_to filter_projects_path(visibility_level: nil) do
Any
- Gitlab::VisibilityLevel.values.each do |level|
- %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ %li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' }
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
@@ -34,7 +34,7 @@
Any
- @tags.each do |tag|
- %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ %li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
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/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
new file mode 100644
index 00000000000..41f54f6bf42
--- /dev/null
+++ b/app/views/groups/_home_panel.html.haml
@@ -0,0 +1,17 @@
+.group-home-panel.text-center
+ %div{ class: container_class }
+ .avatar-container.s70.group-avatar
+ = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
+ %h1.group-title
+ @#{@group.path}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
+
+ - if @group.description.present?
+ .group-home-desc
+ = markdown_field(@group, :description)
+
+ - if current_user
+ .group-buttons
+ = render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
new file mode 100644
index 00000000000..b2097e88741
--- /dev/null
+++ b/app/views/groups/_show_nav.html.haml
@@ -0,0 +1,7 @@
+%ul.nav-links
+ = nav_link(page: group_path(@group)) do
+ = link_to group_path(@group) do
+ Projects
+ = nav_link(page: subgroups_group_path(@group)) do
+ = link_to subgroups_group_path(@group) do
+ Subgroups
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/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index b185b81db7f..5b1a4630c56 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -3,7 +3,7 @@
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
.help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
+ Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
@@ -16,7 +16,7 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this group and all of its projects.
+ On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-create btn-block"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index f4c432a095a..2e4e4511bb6 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -7,7 +7,7 @@
- if can?(current_user, :admin_group_member, @group)
.project-members-new.append-bottom-default
%p.clearfix
- Add new user to
+ Add new member to
%strong= @group.name
= render "new_group_member"
@@ -15,7 +15,7 @@
.append-bottom-default.clearfix
%h5.member.existing-title
- Existing users
+ Existing members
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
@@ -24,7 +24,7 @@
= render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
- Users with access to
+ Members with access to
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
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/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fb6f0da28f8..e66a8e0a3b3 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,8 @@
= render "header_title"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
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/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 d256d14609e..18997baa998 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,58 +1,20 @@
- @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")
-.group-home-panel.text-center
- %div{ class: container_class }
- .avatar-container.s70.group-avatar
- = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- %h1.group-title
- @#{@group.path}
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+= render 'groups/head'
+= render 'groups/home_panel'
- - if @group.description.present?
- .group-home-desc
- = markdown_field(@group, :description)
-
- - if current_user
- .group-buttons
- = render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
.groups-header{ class: container_class }
.top-area
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- All Projects
- - if @shared_projects.present?
- %li
- = link_to "#shared", 'data-toggle' => 'tab' do
- Shared Projects
- - if @nested_groups.present?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Subgroups
+ = 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
New Project
- .tab-content
- .tab-pane.active#projects
- = render "projects", projects: @projects
-
- - if @shared_projects.present?
- .tab-pane#shared
- = render "shared_projects", projects: @shared_projects
-
- - if @nested_groups.present?
- .tab-pane#groups
- %ul.content-list
- = render partial: 'shared/groups/group', collection: @nested_groups
+ = render "projects", projects: @projects
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
new file mode 100644
index 00000000000..be809083139
--- /dev/null
+++ b/app/views/groups/subgroups.html.haml
@@ -0,0 +1,21 @@
+- @no_container = true
+
+= render 'head'
+= render 'groups/home_panel'
+
+.groups-header{ class: container_class }
+ .top-area
+ = render 'groups/show_nav'
+ .nav-controls
+ = form_tag request.path, method: :get do |f|
+ = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
+ - if can? current_user, :admin_group, @group
+ = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
+ New Subgroup
+
+ - if @nested_groups.present?
+ %ul.content-list
+ = render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
+ - else
+ .nothing-here-block
+ There are no subgroups to show.
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index b74cc822295..2684f16c373 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -79,6 +79,14 @@
%td.shortcut
.key esc
%td Go back
+ %tbody
+ %tr
+ %th
+ %th Project File
+ %tr
+ %td.shortcut
+ .key y
+ %td Go to file permalink
.col-lg-4
%table.shortcut-mappings
@@ -143,7 +151,7 @@
.key g
.key b
%td
- Go to builds
+ Go to jobs
%tr
%td.shortcut
.key g
@@ -155,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/ui.html.haml b/app/views/help/ui.html.haml
index dd1df46792b..87f9b503989 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -528,7 +528,7 @@
- blob = Snippet.new(content: "Wow\nSuch\nFile")
.example
.file-holder
- .file-title
+ .js-file-title.file-title
Awesome file
.file-actions
.btn-group
diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml
index 36f8069c1f7..ada5f99f4e2 100644
--- a/app/views/import/base/unauthorized.js.haml
+++ b/app/views/import/base/unauthorized.js.haml
@@ -4,7 +4,7 @@
import_button = tr.find(".btn-import")
origin_target = target_field.text()
project_name = "#{@project_name}"
- origin_namespace = "#{@target_namespace.path}"
+ origin_namespace = "#{@target_namespace.full_path}"
target_field.empty()
target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>")
target_field.append("<input type='text' name='target_namespace' />")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 7f1b9ee7141..e18bd47798b 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -82,7 +82,7 @@
rather than Git. Please convert
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
- = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
+ = link_to 'import flow', status_import_bitbucket_path
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 96831874144..fcd30c8c765 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -2,7 +2,7 @@ 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
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index cefe0066a8f..5c5be03a7cd 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" }
+%li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
= link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 3096f0ee19e..f6d8bb08a64 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -23,16 +23,20 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag 'favicon.ico'
+ = favicon_link_tag favicon
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
- = javascript_include_tag "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
+ = yield :project_javascripts
+
= csrf_meta_tags
- unless browser.safari?
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 54d02ee8e4b..a35a918d501 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,21 +1,4 @@
-.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
- .sidebar-wrapper.nicescroll
- .sidebar-action-buttons
- .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
- %span.sr-only Toggle navigation
- = icon('bars')
-
- %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
- %span.sr-only Toggle navigation pinning
- = icon('fw thumb-tack')
-
- - if defined?(sidebar) && sidebar
- = render "layouts/nav/#{sidebar}"
- - elsif current_user
- = render 'layouts/nav/dashboard'
- - else
- = render 'layouts/nav/explore'
-
+.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
.container-fluid
diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml
new file mode 100644
index 00000000000..77c77dc6754
--- /dev/null
+++ b/app/views/layouts/_recaptcha_verification.html.haml
@@ -0,0 +1,23 @@
+- humanized_resource_name = spammable.class.model_name.human.downcase
+- resource_name = spammable.class.model_name.singular
+
+%h3.page-title
+ Anti-spam verification
+%hr
+
+%p
+ #{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
+
+= form_for form do |f|
+ .recaptcha
+ - params[resource_name].each do |field, value|
+ = hidden_field(resource_name, field, value: value)
+ = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags
+
+ -# Yields a block with given extra params.
+ = yield
+
+ .row-content-block.footer-block
+ = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 935517d4913..36543edc040 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,12 +1,9 @@
!!! 5
%html{ lang: "en", class: "#{page_class}" }
= render "layouts/head"
- %body{ class: "#{user_application_theme}", 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
- -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
- = yield :scripts_body_top
-
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 9ecc0d11c95..6f4f2dbea3a 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,10 +1,16 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+%header.navbar.navbar-gitlab{ class: nav_header_class }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
- %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
- %span.sr-only Toggle navigation
- = icon('bars')
+ .dropdown.global-dropdown
+ %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.sr-only Toggle navigation
+ = icon('bars')
+ .dropdown-menu-nav.global-dropdown-menu
+ - if current_user
+ = render 'layouts/nav/dashboard'
+ - else
+ = render 'layouts/nav/explore'
%button.navbar-toggle{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v')
@@ -13,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
@@ -29,7 +35,11 @@
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
- = todos_pending_count
+ = 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',
@@ -45,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" }
@@ -55,13 +63,12 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %h1.title= title
-
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
+ %h1.title= title
+
= yield :header_content
= render 'shared/outdated_browser'
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 205d23178d2..15285ee32a3 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,41 +1,39 @@
-.nav-sidebar
- %ul.nav
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- %span
- Projects
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
- %span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to dashboard_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- %span
- Issues
- %span.count= 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
- %span.count= 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
+%ul
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ %span
+ Projects
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ %span
+ Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
%span
- Snippets
-
- = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do
+ Koding
+ = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+ = link_to dashboard_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
+ %span
+ Milestones
+ = nav_link(path: 'dashboard#issues') do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+ %span
+ Issues
+ .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
+ .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
- About GitLab CE
+ Snippets
+ %li.divider
+ %li
+ = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index e5bda7b3a6f..3a1fcd00e9c 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-sidebar
+%ul
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
%span
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f3539fd372d..a6e96942021 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -5,23 +5,11 @@
.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 +21,7 @@
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
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index a8bbd67de80..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,22 +55,45 @@
%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: 'Builds', class: 'shortcuts-builds' do
- Builds
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ Jobs
-# Shortcut to commits page
- if project_nav_tab? :commits
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 c6df66d2c3c..00000000000
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ /dev/null
@@ -1,36 +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: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
- %span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
- %span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
- %span
- Triggers
- = nav_link(controller: :pipelines_settings) do
- = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
- %span
- CI/CD Pipelines
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 277eb71ea73..f5e7ea7710d 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -3,7 +3,7 @@
- header_title project_title(@project) unless header_title
- nav "project"
-- content_for :scripts_body_top do
+- content_for :project_javascripts do
- project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index a744c4be9d6..060b50ffc69 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,6 +1,6 @@
- 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 (build failed)
+ GitLab (job failed)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ 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
index 9d497983498..2a94688a6b0 100644
--- a/app/views/notify/build_fail_email.text.erb
+++ b/app/views/notify/build_fail_email.text.erb
@@ -1,4 +1,4 @@
-Build failed for <%= @project.name %>
+Job failed for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 8c2e6db1426..ca0eaa96a9d 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -1,6 +1,6 @@
- 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 (build successful)
+ GitLab (job successful)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ 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
index c5ed4f84861..445cd46e64f 100644
--- a/app/views/notify/build_success_email.text.erb
+++ b/app/views/notify/build_success_email.text.erb
@@ -1,4 +1,4 @@
-Build successful for <%= @project.name %>
+Job successful for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
index f495a2e5486..741c7f344c8 100644
--- a/app/views/notify/links/ci/builds/_build.text.erb
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -1 +1 @@
-Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
+Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
index 8e89c52a1f3..af8924bad57 100644
--- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
@@ -1 +1 @@
-Build #<%= build.id %>
+Job #<%= build.id %>
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 943ebdaeffe..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_tag('profile/profile_bundle.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 14b330d16ad..8a994f6d600 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -82,7 +82,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
%hr
- if current_user.can_change_username?
@@ -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 5c5e5940365..51c4e8e5a73 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,6 +34,11 @@
.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 60a561c9f9c..0645ecad496 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -24,76 +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 date = $('#personal_access_token_expires_at').val();
-
- var datepicker = $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- minDate: 0
- });
-
$("#created-personal-access-token").click(function() {
this.select();
});
-
- $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index feadd863b00..df0a0212f3d 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -4,19 +4,6 @@
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- Application theme
- %p
- This setting allows you to customize the appearance of the site, e.g. the sidebar.
- .col-lg-9.application-theme
- - Gitlab::Themes.each do |theme|
- = label_tag do
- .preview{ class: theme.css_class }
- = f.radio_button :theme_id, theme.id
- = theme.name
- .col-sm-12
- %hr
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
Syntax highlighting theme
%p
This setting allow you to customize the appearance of the syntax.
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 8966dd3fd86..431ab9d052b 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -1,7 +1,3 @@
-// Remove body class for any previous theme, re-add current one
-$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
-$('body').addClass('<%= user_application_theme %>')
-
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
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/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
index e2b73cee5a9..a41791f0eca 100644
--- a/app/views/projects/_customize_workflow.html.haml
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -3,6 +3,6 @@
%h4
Customize your workflow!
%p
- Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
+ Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
= link_to "Get started", edit_project_path(@project), class: "btn btn-success"
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/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 1b9d87e9969..79a0dc1b959 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -28,9 +28,11 @@
.project-clone-holder
= render "shared/clone_panel"
- - if current_user && can?(current_user, :download_code, @project)
- = render 'projects/buttons/download', project: @project, ref: @ref
- = render 'projects/buttons/dropdown'
+ - if current_user
+ - if can?(current_user, :download_code, @project)
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render 'projects/buttons/dropdown'
+ = render 'projects/buttons/koding'
+
= render 'shared/notifications/button', notification_setting: @notification_setting
- = render 'projects/buttons/koding'
= render 'shared/members/access_request_buttons', source: @project
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 1c3bccccb5c..a08436715d2 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -10,6 +10,7 @@
- if @project && event.project != @project
%span at
%strong= link_to_project event.project
+ = clipboard_button(clipboard_text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard')
#{time_ago_with_tooltip(event.created_at)}
.pull-right
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 1a1327fb53c..188198c47d5 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -2,12 +2,12 @@
.form-group
.checkbox.builds-feature
- = form.label :only_allow_merge_if_build_succeeds do
- = form.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the 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
- Builds need to be configured to enable this feature.
+ Pipelines need to be configured to enable this feature.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')
.checkbox
= form.label :only_allow_merge_if_all_discussions_are_resolved do
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/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index d0ff14e45e6..edf55d59f28 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
.top-block.row-content-block.clearfix
.pull-right
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 23f54553014..8a40281e28c 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,7 +7,7 @@
#blob-content-holder.tree-holder
.file-holder
- .file-title
+ .js-file-title.file-title
= blob_icon @blob.mode, @blob.name
%strong
= @path
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index ff893ea74e1..14d42f7d9ec 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,4 +1,8 @@
-.btn-group.tree-btn-group
+- if @environment
+ .btn-group<
+ = view_on_environment_button(@commit.sha, @path, @environment)
+
+.btn-group{ role: "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
@@ -8,14 +12,14 @@
class: 'btn btn-sm'
- else
= link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
- class: 'btn btn-sm' unless @blob.empty?
+ 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'
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user
- .btn-group{ role: "group" }
+ .btn-group{ role: "group" }<
- if blob_text_viewable?(@blob)
= edit_blob_link
= replace_blob_link
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index f75f438ee4f..bf8801bb1e3 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,18 +18,19 @@
- 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
- .file-title
- = blob_icon blob.mode, blob.name
- %strong
- = blob.name
- %small
- = number_to_human_size(blob_size(blob))
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = blob_icon blob.mode, blob.name
+ %strong.file-title-name
+ = blob.name
+ %small
+ = number_to_human_size(blob_size(blob))
.file-actions.hidden-xs
= render "actions"
- = render blob, blob: blob
+ = render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 228ac61fc8c..e7adef5558a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,5 +1,5 @@
.file-holder.file.append-bottom-default
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
.editor-ref
= icon('code-fork')
= ref
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 538f8591f13..d1d448f0d4c 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -9,22 +9,22 @@
- 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.loc
+ - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
%tr.line_holder{ id: @form.to, class: line_class }
- = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true
+ = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a5dcd93f42e..8853801016b 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index b6ed9518c48..e0ce8cc9601 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
%h3.page-title
New File
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 356bd50f7f3..3ae78387938 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,12 +3,12 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+ = page_specific_javascript_bundle_tag('common_vue')
+ = 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"
= render "projects/issues/head"
@@ -24,5 +24,13 @@
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
+ %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
+ "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project),
+ "milestone-path" => milestones_filter_dropdown_path,
+ "label-path" => labels_filter_path,
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":project-id" => @project.try(:id) }
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index a2e5118a9f3..72bce4049de 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -29,6 +29,7 @@
":loading" => "list.loading",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index 34fdb1f6a74..4a4dd84d5d2 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -2,41 +2,23 @@
.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",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
+ ":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 e4c2aff46ec..00000000000
--- a/app/views/projects/boards/components/_card.html.haml
+++ /dev/null
@@ -1,28 +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)" }
- %h4.card-title
- = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
- %a{ ":href" => 'issueLinkBase + "/" + issue.id',
- ":title" => "issue.title" }
- {{ issue.title }}
- .card-footer
- %span.card-number{ "v-if" => "issue.id" }
- = precede '#' do
- {{ issue.id }}
- %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
- ":title" => '"Assigned to " + issue.assignee.name',
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" }
- %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
- type: "button",
- "v-if" => "(!list.label || label.id !== list.label.id)",
- "@click" => "filterByLabel(label, $event)",
- ":style" => "{ backgroundColor: label.color, color: label.textColor }",
- ":title" => "label.description",
- data: { container: 'body' } }
- {{ label.title }}
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index df7fa9ddaf2..24d76da6f06 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -22,3 +22,5 @@
= render "projects/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
+ %remove-btn{ ":issue" => "issue",
+ ":list" => "list" }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19ffe73a08d..ae63f8184df 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -31,7 +31,7 @@
- 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 736b485bf06..7eb17e887e7 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,7 +1,7 @@
-.content-block.build-header
+.content-block.build-header.top-area
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
- Build
+ Job
%strong.js-build-id ##{@build.id}
in pipeline
= link_to pipeline_path(@build.pipeline) do
@@ -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 build", 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/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 37bf085130a..78720d88e4e 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,8 +1,8 @@
- builds = @build.pipeline.builds.to_a
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
- Build
+ Job
%strong ##{@build.id}
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
@@ -17,7 +17,7 @@
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
- Build artifacts
+ Job artifacts
- if @build.artifacts_expired?
%p.build-detail-row
The artifacts were removed
@@ -42,9 +42,9 @@
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
- Build details
+ Job details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -136,4 +136,4 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' }
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 028664f5bba..acfdb250aff 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -2,14 +2,14 @@
- if builds.blank?
%div
- .nothing-here-block No builds to show
+ .nothing-here-block No jobs to show
- else
.table-holder
%table.table.ci-table.builds-page
%thead
%tr
%th Status
- %th Build
+ %th Job
%th Pipeline
- if admin
%th Project
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index c623e39b21f..5ffc0e20d10 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Builds"
+- page_title "Jobs"
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -14,7 +14,7 @@
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index c613e473e4c..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})", "Builds"
-- trace_with_state = @build.trace_with_state
+- page_title "#{@build.name} (##{@build.id})", "Jobs"
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
@@ -12,14 +11,14 @@
.bs-callout.bs-callout-warning
%p
- if no_runners_for_project?(@build.project)
- This build is stuck, because the project doesn't have any runners online assigned to it.
+ This job is stuck, because the project doesn't have any runners online assigned to it.
- elsif @build.tags.any?
- This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+ This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
%span.label.label-primary
= tag
- else
- This build is stuck, because you don't have any active runners that can run this build.
+ This job is stuck, because you don't have any active runners that can run this job.
%br
Go to
@@ -37,14 +36,14 @@
- environment = environment_for_build(@build.project, @build)
- if @build.success? && @build.last_deployment.present?
- if @build.last_deployment.last?
- This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
- else
- This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
View the most recent deployment #{deployment_link(environment.last_deployment)}.
- elsif @build.complete? && !@build.success?
- The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed.
- else
- This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
@@ -52,9 +51,9 @@
- if @build.erased?
.erased.alert.alert-warning
- if @build.erased_by_user?
- Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
+ Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
- Build has been erased #{time_ago_with_tooltip(@build.erased_at)}
+ Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 762ff34a9ec..b560ed21f1d 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -8,19 +8,19 @@
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li.dropdown-header Source code
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download zip
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar.gz
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar.bz2
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar
@@ -36,6 +36,6 @@
%li.dropdown-header Previous Artifacts
- artifacts.each do |job|
%li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download '#{job.name}'
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index c1e496455d1..09286a1b3c6 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -32,10 +32,10 @@
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- if build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
.label-container
- if build.tags.any?
@@ -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 818a70f38f1..00000000000
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ /dev/null
@@ -1,109 +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 build 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.stage-cell
- - pipeline.stages.each do |stage|
- - if stage.status
- - detailed_status = stage.detailed_status(current_user)
- - icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
-
- .stage-container.dropdown.js-mini-pipeline-graph
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
- = icon('caret-down')
-
- %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- .arrow-up
- .js-builds-dropdown-list.scrollable-menu
-
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
-
-
- %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 build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' }
- = 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' 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..d001e01609a 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
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 08d3443b3d0..c2b32a22170 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -3,7 +3,7 @@
.pull-right
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+ = link_to "Retry", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'js-retry-button btn btn-grouped btn-primary', method: :post
- if pipeline.builds.running_or_pending.any?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
@@ -13,7 +13,7 @@
Pipeline
= link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
with
- = pluralize pipeline.statuses.count(:id), "build"
+ = pluralize pipeline.statuses.count(:id), "job"
- if pipeline.ref
for
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
@@ -44,7 +44,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 1164627fa11..da5a676274f 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,15 +1,8 @@
-%div
- - if pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .table-holder.pipelines
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
+- disable_initialization = local_assigns.fetch(:disable_initialization, false)
+#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
+ endpoint: endpoint,
+} }
+
+- 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/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 89968cf4e0d..ac93eac41ac 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -2,4 +2,4 @@
= render 'commit_box'
= render 'ci_menu'
-= render 'pipelines_list', pipelines: @pipelines
+= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 7afd3d80ef5..d5fc283aa8d 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,7 +9,7 @@
= render "ci_menu"
- else
.block-connector
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
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 d94f23f5a38..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"
@@ -22,17 +21,14 @@
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.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/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d76d48187cd..08236216421 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -23,6 +23,4 @@
- if @merge_request.present?
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button?
- = link_to create_mr_path, class: 'prepend-left-10 btn' do
- = icon("plus")
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 9c8f58d4aea..0dfc9fe20ed 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
.light-well
.center
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 479ce44f378..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_tag("cycle_analytics/cycle_analytics_bundle.js")
+ = 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/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index b87b79b170e..5c38b5ad9c0 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -15,10 +15,13 @@
%a.click-to-expand
Click to expand it.
- elsif diff_file.diff_lines.length > 0
+ - total_lines = 0
+ - if blob.lines.any?
+ - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size
- if diff_view == :parallel
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
+ = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
- else
- = render "projects/diffs/text_file", diff_file: diff_file
+ = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
- else
- if diff_file.mode_changed?
.nothing-here-block File mode changed
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 58c20e225c6..4b49bed835f 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index fc478ccc995..0232a09b4a8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,8 @@
+- environment = local_assigns.fetch(:environment, nil)
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
- .file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
@@ -13,6 +15,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file.new_path, project)
+ = view_file_button(diff_commit.id, diff_file.new_path, project)
+ = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index ddec775b789..f809c52c367 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -10,13 +10,13 @@
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path
&rarr;
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= diff_file.new_path
- if diff_file.deleted_file
deleted
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index cd18ba2ed00..62135d3ae32 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,8 +1,11 @@
- 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
@@ -11,12 +14,14 @@
%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 f361204ecac..e7758c8bdfa 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,41 +1,55 @@
/ Side-by-side diff view
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- - last_line = 0
- 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
- - if left.meta?
+ - case left.type
+ - when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
+ - when '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
%td.line_content.parallel
- if right
- - if right.meta?
+ - case right.type
+ - when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
+ - when '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 !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true, view: :parallel
+ - 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
+ %tr.line_holder.parallel
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index f1d2d4bf268..ebd1a914ee7 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,13 +4,14 @@
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- - last_line = 0
- discussions = @grouped_diff_discussions unless @diff_notes_disabled
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
locals: { diff_file: diff_file, discussions: discussions }
- - last_line = diff_file.highlighted_diff_lines.last.new_pos
- - if !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true
+ - if !diff_file.new_file && !diff_file.deleted_file && diff_file.highlighted_diff_lines.any?
+ - last_line = diff_file.highlighted_diff_lines.last
+ - if last_line.new_pos < total_lines
+ %tr.line_holder
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 4a0ce995165..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
@@ -6,7 +7,7 @@
.col-lg-9
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
- %fieldset.append-bottom-0
+ %fieldset
.row
.form-group.col-md-9
= f.label :name, class: 'label-light', for: 'project_name_edit' do
@@ -33,7 +34,7 @@
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
- %fieldset.append-bottom-0
+ %fieldset
%h5.prepend-top-0
Sharing &amp; Permissions
.form_group.prepend-top-20.sharing-and-permissions
@@ -63,7 +64,7 @@
.row
.col-md-9.project-feature.nested
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
@@ -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 }
@@ -133,6 +134,7 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
+
.row.prepend-top-default
%hr
.row.prepend-top-default
@@ -180,7 +182,7 @@
%p
The following items will NOT be exported:
%ul
- %li Build traces and artifacts
+ %li Job traces and artifacts
%li LFS objects
%li Container registry images
%li CI variables
@@ -231,7 +233,7 @@
.form-group
.input-group
.input-group-addon
- #{URI.join(root_url, @project.namespace.path)}/
+ #{URI.join(root_url, @project.namespace.full_path)}/
= f.text_field :path, class: 'form-control'
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
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/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
index 69848123c17..14a2d627203 100644
--- a/app/views/projects/environments/_stop.html.haml
+++ b/app/views/projects/environments/_stop.html.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
new file mode 100644
index 00000000000..4b101447bc0
--- /dev/null
+++ b/app/views/projects/environments/folder.html.haml
@@ -0,0 +1,14 @@
+- @no_container = true
+- page_title "Environments"
+= 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,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "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") } }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 8c728eb0f6a..80d2b6f5d95 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,7 +3,8 @@
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag("environments/environments_bundle.js")
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
@@ -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..f8e94ca98ae
--- /dev/null
+++ b/app/views/projects/environments/metrics.html.haml
@@ -0,0 +1,21 @@
+- @no_container = true
+- page_title "Metrics for environment", @environment.name
+= 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 f3179dce5f2..f463a429f65 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,14 +8,15 @@
%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)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ - 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
@@ -32,7 +33,7 @@
%tr
%th ID
%th Commit
- %th Build
+ %th Job
%th Created
%th.hidden-xs
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 431253c1299..ef0dd0eda3c 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -4,7 +4,7 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
- = page_specific_javascript_tag("terminal/terminal_bundle.js")
+ = page_specific_javascript_bundle_tag("terminal")
%div{ class: container_class }
.top-area
@@ -16,6 +16,8 @@
.col-sm-6
.nav-controls
+ = link_to @environment.external_url, class: 'btn btn-default' do
+ = icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class }
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
deleted file mode 100644
index 1a62a6a809c..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_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
- = 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/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
deleted file mode 100644
index 431657c4dcb..00000000000
--- a/app/views/projects/graphs/ci/_builds.haml
+++ /dev/null
@@ -1,56 +0,0 @@
-%h4 Build charts
-%p
- &nbsp;
- %span.cgreen
- = icon("circle")
- success
- &nbsp;
- %span.cgray
- = icon("circle")
- all
-
-.prepend-top-default
- %p.light
- Builds for last week
- (#{date_from_to(Date.today - 7.days, Date.today)})
- %canvas#weekChart{ height: 200 }
-
-.prepend-top-default
- %p.light
- Builds for last month
- (#{date_from_to(Date.today - 30.days, Date.today)})
- %canvas#monthChart{ height: 200 }
-
-.prepend-top-default
- %p.light
- Builds for last year
- %canvas#yearChart.padded{ height: 250 }
-
-- [:week, :month, :year].each do |scope|
- :javascript
- var data = {
- labels : #{@charts[scope].labels.to_json},
- datasets : [
- {
- fillColor : "#7f8fa4",
- strokeColor : "#7f8fa4",
- pointColor : "#7f8fa4",
- pointStrokeColor : "#EEE",
- data : #{@charts[scope].total.to_json}
- },
- {
- fillColor : "#44aa22",
- strokeColor : "#44aa22",
- pointColor : "#44aa22",
- pointStrokeColor : "#fff",
- data : #{@charts[scope].success.to_json}
- }
- ]
- }
- var ctx = $("##{scope}Chart").get(0).getContext("2d");
- var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
- if (window.innerWidth < 768) {
- // Scale fonts if window width lower than 768px (iPad portrait)
- options.scaleFontSize = 8
- }
- new Chart(ctx).Line(data, options);
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/group_links/_index.html.haml b/app/views/projects/group_links/_index.html.haml
index 99d0df2ac34..b6116dbec41 100644
--- a/app/views/projects/group_links/_index.html.haml
+++ b/app/views/projects/group_links/_index.html.haml
@@ -39,7 +39,7 @@
= icon("folder-open-o", class: "settings-list-icon")
.pull-left
= link_to group do
- = group.name
+ = group.full_name
%br
up to #{group_link.human_access}
- if group_link.expires?
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c2f4457b60b..5d4e593e4ef 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-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, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
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/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index f3be343daae..0e3902c066a 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,60 +1,46 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- - if @bulk_edit
- .issue-check
- = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-box
+ - if @bulk_edit
+ .issue-check
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-info-container
+ .issue-title.title
+ %span.issue-title-text
+ = confidential_icon(issue)
+ = link_to issue.title, issue_path(issue)
+ %ul.controls
+ - if issue.closed?
+ %li
+ CLOSED
- .issue-title.title
- %span.issue-title-text
- = confidential_icon(issue)
- = link_to issue.title, issue_path(issue)
- %ul.controls
- - if issue.closed?
- %li
- CLOSED
+ - if issue.assignee
+ %li
+ = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- - if issue.assignee
- %li
- = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+ = render 'shared/issuable_meta_data', issuable: issue
- - upvotes, downvotes = issue.upvotes, issue.downvotes
- - if upvotes > 0
- %li
- = icon('thumbs-up')
- = upvotes
+ .issue-info
+ #{issuable_reference(issue)} &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
+ - if issue.milestone
+ &nbsp;
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
+ = icon('clock-o')
+ = issue.milestone.title
+ - if issue.due_date
+ %span{ class: "#{'cred' if issue.overdue?}" }
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
+ - if issue.labels.any?
+ &nbsp;
+ - issue.labels.each do |label|
+ = link_to_label(label, subject: issue.project, css_class: 'label-link')
+ - if issue.tasks?
+ &nbsp;
+ %span.task-status
+ = issue.task_status
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = issue.notes.user.count
- %li
- = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .issue-info
- #{issuable_reference(issue)} &middot;
- opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
- by #{link_to_member(@project, issue.author, avatar: false)}
- - if issue.milestone
- &nbsp;
- = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
- = icon('clock-o')
- = issue.milestone.title
- - if issue.due_date
- %span{ class: "#{'cred' if issue.overdue?}" }
- &nbsp;
- = icon('calendar')
- = issue.due_date.to_s(:medium)
- - if issue.labels.any?
- &nbsp;
- - issue.labels.each do |label|
- = link_to_label(label, subject: issue.project)
- - if issue.tasks?
- &nbsp;
- %span.task-status
- = issue.task_status
-
- .pull-right.issue-updated-at
- %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ .pull-right.issue-updated-at
+ %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
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 5fbed8b9ab8..7b7d7b1e00e 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -7,20 +7,18 @@
= render "projects/issues/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js')
+ = 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')
+ = link_to params.merge(rss_url_options), 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,
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 11636d7ebc7..d39f36e94c7 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_tag('lib/vue_resource.js')
.clearfix.detail-page-header
.issuable-header
@@ -35,12 +33,12 @@
= 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 }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = 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 }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = 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? && current_user.admin?
+ - 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'
@@ -48,9 +46,9 @@
= 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 }, status_only: true, 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 }, status_only: true, 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? && current_user.admin?
+ = 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'
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
new file mode 100644
index 00000000000..6da7c317f3a
--- /dev/null
+++ b/app/views/projects/issues/verify.html.haml
@@ -0,0 +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_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/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 29f861c09c6..8d4a91cb64c 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,6 +3,9 @@
- hide_class = ''
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
- if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class }
.top-area.adjust
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/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 513f0818169..11b7aaec704 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -3,73 +3,59 @@
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
- .merge-request-title.title
- %span.merge-request-title-text
- = link_to merge_request.title, merge_request_path(merge_request)
- %ul.controls
- - if merge_request.merged?
- %li
- MERGED
- - elsif merge_request.closed?
- %li
- = icon('ban')
- CLOSED
-
- - if merge_request.head_pipeline
- %li
- = render_pipeline_status(merge_request.head_pipeline)
-
- - if merge_request.open? && merge_request.broken?
- %li
- = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
- = icon('exclamation-triangle')
-
- - if merge_request.assignee
- %li
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
-
- - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes
- - if upvotes > 0
- %li
- = icon('thumbs-up')
- = upvotes
-
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = merge_request.related_notes.user.count
- %li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .merge-request-info
- #{issuable_reference(merge_request)} &middot;
- opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
- by #{link_to_member(@project, merge_request.author, avatar: false)}
- - if merge_request.target_project.default_branch != merge_request.target_branch
- &nbsp;
- = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
- = icon('code-fork')
- = merge_request.target_branch
-
- - if merge_request.milestone
- &nbsp;
- = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
- = icon('clock-o')
- = merge_request.milestone.title
-
- - if merge_request.labels.any?
- &nbsp;
- - merge_request.labels.each do |label|
- = link_to_label(label, subject: merge_request.project, type: :merge_request)
-
- - if merge_request.tasks?
- &nbsp;
- %span.task-status
- = merge_request.task_status
-
- .pull-right.hidden-xs
- %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ .issue-info-container
+ .merge-request-title.title
+ %span.merge-request-title-text
+ = link_to merge_request.title, merge_request_path(merge_request)
+ %ul.controls
+ - if merge_request.merged?
+ %li
+ MERGED
+ - elsif merge_request.closed?
+ %li
+ = icon('ban')
+ CLOSED
+
+ - if merge_request.head_pipeline
+ %li
+ = render_pipeline_status(merge_request.head_pipeline)
+
+ - if merge_request.open? && merge_request.broken?
+ %li
+ = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = icon('exclamation-triangle')
+
+ - if merge_request.assignee
+ %li
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+
+ = render 'shared/issuable_meta_data', issuable: merge_request
+
+ .merge-request-info
+ #{issuable_reference(merge_request)} &middot;
+ opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, merge_request.author, avatar: false)}
+ - if merge_request.target_project.default_branch != merge_request.target_branch
+ &nbsp;
+ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
+ = icon('code-fork')
+ = merge_request.target_branch
+
+ - if merge_request.milestone
+ &nbsp;
+ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
+ = icon('clock-o')
+ = merge_request.milestone.title
+
+ - if merge_request.labels.any?
+ &nbsp;
+ - merge_request.labels.each do |label|
+ = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
+
+ - if merge_request.tasks?
+ &nbsp;
+ %span.task-status
+ = merge_request.task_status
+
+ .pull-right.hidden-xs
+ %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
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_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
index 74367ab9b7b..627fc4e9671 100644
--- a/app/views/projects/merge_requests/_new_diffs.html.haml
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -1 +1 @@
-= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index d3c013b3f21..e7fcac4c477 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,17 +46,12 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render "projects/merge_requests/show/pipelines"
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= 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 9585a9a3ad4..c8f097c69da 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,10 +3,10 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/vue_resource.js')
- = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('diff_notes')
-.merge-request{ 'data-url' => merge_request_path(@merge_request) }
+.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -29,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?
@@ -82,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
@@ -94,7 +95,8 @@
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- -# This tab is always loaded via AJAX
+ - if @pipelines.any?
+ = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
@@ -108,10 +110,10 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
- var merge_request;
-
- merge_request = new MergeRequest({
- action: "#{controller.action_name}"
+ $(function () {
+ new MergeRequest({
+ action: "#{controller.action_name}"
+ });
});
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
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 ebef2157d34..51d59280be8 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,7 +1,7 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/vue_resource.js')
- = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+ = 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"
@@ -23,7 +23,7 @@
.files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
.diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
- .file-title
+ .js-file-title.file-title
%i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 144b3a9c8c8..8a96c8dacf6 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,21 +2,21 @@
- @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
+ = page_specific_javascript_bundle_tag('filtered_search')
+
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
-
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request
- = render 'shared/issuable/filter', type: :merge_requests
+ = render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
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/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 5f048d04b27..7f0913ea516 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index afe3f3430c6..de4aa255bbd 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1 +1,3 @@
-= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
+- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 0e3af62ebc2..1298376ac25 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,16 +1,21 @@
- if @pipeline
.mr-widget-heading
- - %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
- = ci_icon_for_status(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 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
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- for
- = succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
+ - if @pipeline.stages.any?
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
+ %span
+ for
+ = succeed "." do
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
@@ -21,9 +26,9 @@
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
= ci_icon_for_status(status)
%span
- CI build
+ CI job
= ci_label_for_status(status)
- for
+ for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
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/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index f07e6b3ad54..0b0fb7854c2 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -16,13 +16,13 @@
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
- normal: "Build {{status}} for \"{{title}}\"",
- preparing: "{{status}} build for \"{{title}}\""
+ normal: "Pipeline {{status}} for \"{{title}}\"",
+ preparing: "{{status}} pipeline for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
- preparing: "{{status}} build",
- normal: "Build {{status}}"
+ preparing: "{{status}} pipeline",
+ normal: "Pipeline {{status}}"
},
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
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 7809e9c8c72..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,7 +1,5 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
-
-- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
+ = page_specific_javascript_bundle_tag('merge_request_widget')
= 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
@@ -11,34 +9,34 @@
.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
The source branch will be removed.
- elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
- = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
+ = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
- .accept-control.right
+ .accept-control
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
@@ -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/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
index 14f51af5360..3979d5fa8ed 100644
--- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -1,6 +1,6 @@
%h4
= icon('exclamation-triangle')
- The build for this merge request failed
+ The pipeline for this merge request failed
%p
- Please retry the build or push a new commit to fix the failure.
+ Please retry the job or push a new commit to fix the failure.
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index 50086767446..909dc52fc06 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -1,5 +1,5 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+ = page_specific_javascript_bundle_tag('merge_request_widget')
%strong
= icon("spinner spin")
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 f70cd09c5f4..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_tag('merge_request_widget/ci_bundle.js')
-
-%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_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), 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/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 c3a6096aa54..b4dde2c86c9 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,6 +3,9 @@
- page_description @milestone.description
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
@@ -16,10 +19,9 @@
Open
.header-text-content
%span.identifier
- Milestone ##{@milestone.iid}
+ %strong
+ Milestone %#{@milestone.iid}
- if @milestone.due_date || @milestone.start_date
- %span.creator
- &middot;
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
@@ -44,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 d8951e69242..ed6077f6c6b 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,6 @@
-- page_title "Network", @ref
+- page_title "Graph", @ref
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/raphael.js')
- = page_specific_javascript_tag('network/network_bundle.js')
+ = page_specific_javascript_bundle_tag('network')
= render "projects/commits/head"
= render "head"
%div{ class: container_class }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 064e92b15eb..2a98bba05ee 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -12,7 +12,7 @@
Create or Import your project from popular Git services
.col-lg-9
= form_for @project, html: { class: 'new_project' } do |f|
- %fieldset.append-bottom-0
+ .row
.form-group.col-xs-12.col-sm-6
= f.label :namespace_id, class: 'label-light' do
%span
@@ -22,7 +22,7 @@
- if current_user.can_select_namespace?
.input-group-addon
= root_url
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
+ = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
@@ -50,7 +50,7 @@
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
@@ -94,9 +94,8 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
- = link_to "(?)", 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
-
+ = 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
= 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/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 6c14f48d41b..81d97eabe65 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -1,7 +1,6 @@
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.comment-toolbar.clearfix
.toolbar-text
- Styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_slash_commands
and
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 09339e520dd..5552086bc50 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,16 +2,19 @@
- 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) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- = link_to_member(note.project, note.author, avatar: false)
- .note-headline-light
+ %a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
+ = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
+ .note-headline-light
+ %span.hidden-xs
+ = note.author.to_reference
- unless note.system
commented
- if note.system
@@ -23,32 +26,34 @@
.note-actions
- access = note_max_access_for_user(note)
- if access
- %span.note-role.hidden-xs= access
+ %span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "project-path" => "#{project_path(note.project)}",
- "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?
@@ -59,7 +64,7 @@
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
@@ -69,12 +74,13 @@
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
- %textarea.hidden.js-task-list-field.original-task-list= note.note
+ %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
.system-note-commit-list-toggler
Toggle commit list
+ %i.fa.fa-angle-down
- if note.attachment.url
.note-attachment
- if note.attachment.image?
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index fbd2bff5bbb..90a150aa74c 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -13,7 +13,7 @@
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view
- - else
+ - elsif !current_user
.disabled-comment.text-center
.disabled-comment-text.inline
Please
@@ -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/_access.html.haml b/app/views/projects/pages/_access.html.haml
new file mode 100644
index 00000000000..82e20eeebb3
--- /dev/null
+++ b/app/views/projects/pages/_access.html.haml
@@ -0,0 +1,13 @@
+- if @project.pages_deployed?
+ .panel.panel-default
+ .panel-heading
+ Access pages
+ .panel-body
+ %p
+ %strong
+ Congratulations! Your pages are served under:
+
+ %p= link_to @project.pages_url, @project.pages_url
+
+ - @project.pages_domains.each do |domain|
+ %p= link_to domain.url, domain.url
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
new file mode 100644
index 00000000000..42d9ef5ccba
--- /dev/null
+++ b/app/views/projects/pages/_destroy.haml
@@ -0,0 +1,12 @@
+- if @project.pages_deployed?
+ - if can?(current_user, :remove_pages, @project)
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove pages
+ .errors-holder
+ .panel-body
+ %p
+ Removing the pages will prevent from exposing them to outside world.
+ .form-actions
+ = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
+ - else
+ .nothing-here-block Only the project owner can remove pages
diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml
new file mode 100644
index 00000000000..ad51fbc6cab
--- /dev/null
+++ b/app/views/projects/pages/_disabled.html.haml
@@ -0,0 +1,4 @@
+.panel.panel-default
+ .nothing-here-block
+ GitLab Pages are disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
new file mode 100644
index 00000000000..4f2dd1a1398
--- /dev/null
+++ b/app/views/projects/pages/_list.html.haml
@@ -0,0 +1,17 @@
+- if can?(current_user, :update_pages, @project) && @domains.any?
+ .panel.panel-default
+ .panel-heading
+ Domains (#{@domains.count})
+ %ul.well-list
+ - @domains.each do |domain|
+ %li
+ .pull-right
+ = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+ .clearfix
+ %span= link_to domain.domain, domain.url
+ %p
+ - if domain.subject
+ %span.label.label-gray Certificate: #{domain.subject}
+ - if domain.expired?
+ %span.label.label-danger Expired
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
new file mode 100644
index 00000000000..7cea5f3e70b
--- /dev/null
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -0,0 +1,7 @@
+- if can?(current_user, :update_pages, @project)
+ .panel.panel-default
+ .panel-heading
+ Domains
+ .nothing-here-block
+ Support for domains and certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
new file mode 100644
index 00000000000..e442e6e9a09
--- /dev/null
+++ b/app/views/projects/pages/_use.html.haml
@@ -0,0 +1,10 @@
+- unless @project.pages_deployed?
+ .panel.panel-info
+ .panel-heading
+ Configure pages
+ .panel-body
+ %p
+ Learn how to upload your static site and have it served by
+ 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
new file mode 100644
index 00000000000..259d5bd63d6
--- /dev/null
+++ b/app/views/projects/pages/show.html.haml
@@ -0,0 +1,28 @@
+- page_title 'Pages'
+= render "projects/settings/head"
+
+%h3.page_title
+ Pages
+
+ - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
+ = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ %i.fa.fa-plus
+ New Domain
+
+%p.light
+ With GitLab Pages you can host 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.
+
+%hr.clearfix
+
+- if Gitlab.config.pages.enabled
+ = render 'access'
+ = render 'use'
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
+ = render 'list'
+ - else
+ = render 'no_domains'
+ = render 'destroy'
+- else
+ = render 'disabled'
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
new file mode 100644
index 00000000000..ca1b41b140a
--- /dev/null
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -0,0 +1,34 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+
+ .form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
+
+ - if Gitlab.config.pages.external_https
+ .form-group
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
+ .col-sm-10
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
+
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+ - else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
+
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
new file mode 100644
index 00000000000..e1477c71d06
--- /dev/null
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -0,0 +1,6 @@
+- page_title 'New Pages Domain'
+%h3.page_title
+ New Pages Domain
+%hr.clearfix
+%div
+ = render 'form'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
new file mode 100644
index 00000000000..876cac0dacb
--- /dev/null
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -0,0 +1,30 @@
+- page_title "#{@domain.domain}", 'Pages Domains'
+
+%h3.page-title
+ Pages Domain
+
+.table-holder
+ %table.table
+ %tr
+ %td
+ Domain
+ %td
+ = link_to @domain.domain, @domain.url
+ %tr
+ %td
+ DNS
+ %td
+ %p
+ To access the domain create a new DNS record:
+ %pre
+ #{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}.
+ %tr
+ %td
+ Certificate
+ %td
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .light
+ missing
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index b10dd47709f..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
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ = nav_link(path: 'builds#index', controller: :builds) do
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
- Builds
+ 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/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 6caa5f16dc6..0605af4fcd3 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -7,9 +7,9 @@
= commit_author_link(@commit)
.header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project)
- - if @pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
- - if @pipeline.builds.running_or_pending.any?
+ - if @pipeline.retryable?
+ = link_to "Retry", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'js-retry-button btn btn-inverted-secondary', method: :post
+ - if @pipeline.cancelable?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
@@ -25,7 +25,7 @@
.well-segment.pipeline-info
.icon-container
= icon('clock-o')
- = pluralize @pipeline.statuses.count(:id), "build"
+ = pluralize @pipeline.statuses.count(:id), "job"
- if @pipeline.ref
from
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
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/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 88af41aa835..53067cdcba4 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -5,7 +5,7 @@
Pipeline
%li.js-builds-tab-link
= link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
- Builds
+ Jobs
%span.badge.js-builds-counter= pipeline.statuses.count
@@ -33,7 +33,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
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/pipelines/charts/_builds.haml b/app/views/projects/pipelines/charts/_builds.haml
new file mode 100644
index 00000000000..b6f453b9736
--- /dev/null
+++ b/app/views/projects/pipelines/charts/_builds.haml
@@ -0,0 +1,56 @@
+%h4 Pipelines charts
+%p
+ &nbsp;
+ %span.cgreen
+ = icon("circle")
+ success
+ &nbsp;
+ %span.cgray
+ = icon("circle")
+ all
+
+.prepend-top-default
+ %p.light
+ Jobs for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+ %canvas#weekChart{ height: 200 }
+
+.prepend-top-default
+ %p.light
+ Jobs for last month
+ (#{date_from_to(Date.today - 30.days, Date.today)})
+ %canvas#monthChart{ height: 200 }
+
+.prepend-top-default
+ %p.light
+ Jobs for last year
+ %canvas#yearChart.padded{ height: 250 }
+
+- [:week, :month, :year].each do |scope|
+ :javascript
+ var data = {
+ labels : #{@charts[scope].labels.to_json},
+ datasets : [
+ {
+ fillColor : "#7f8fa4",
+ strokeColor : "#7f8fa4",
+ pointColor : "#7f8fa4",
+ pointStrokeColor : "#EEE",
+ data : #{@charts[scope].total.to_json}
+ },
+ {
+ fillColor : "#44aa22",
+ strokeColor : "#44aa22",
+ pointColor : "#44aa22",
+ pointStrokeColor : "#fff",
+ data : #{@charts[scope].success.to_json}
+ }
+ ]
+ }
+ var ctx = $("##{scope}Chart").get(0).getContext("2d");
+ var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8
+ }
+ new Chart(ctx).Line(data, options);
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 df36279ed75..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' if @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' if @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' if @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' if @scope == 'tags') }>
+ %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
@@ -36,32 +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) } }
- - if @pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .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_tag('vue_pipelines_index/index.js')
+= 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/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
index 22a3b884520..43bbd735059 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -25,3 +25,10 @@
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', badge.to_html)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ AsciiDoc
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.adoc', badge.to_asciidoc)
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
new file mode 100644
index 00000000000..132f6372e40
--- /dev/null
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -0,0 +1,96 @@
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ CI/CD Pipelines
+ .col-lg-9
+ = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
+ %fieldset.builds-feature
+ - unless @repository.gitlab_ci_yml
+ .form-group
+ %p Pipelines need to be configured before you can begin using Continuous Integration.
+ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ %hr
+ .form-group.append-bottom-default
+ = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used by the Runner to checkout the project
+
+ %hr
+ .form-group
+ %h5.prepend-top-0
+ Git strategy for pipelines
+ %p
+ Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr
+ Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr
+ Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+
+ %hr
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block
+ Per job in minutes. If a job passes this threshold, it will be marked as failed.
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
+
+ %hr
+ .form-group
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public pipelines
+ .help-block
+ Allow everyone to access pipelines for public and internal projects
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
+
+ %hr
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
+ %span.input-group-addon /
+ %p.help-block
+ A regular expression that will be used to find the test coverage
+ output in the job trace. Leave blank to disable
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+ %li
+ gcovr (C/C++) -
+ %code ^TOTAL.*\s+(\d+\%)$
+ %li
+ tap --coverage-report=text-summary (NodeJS) -
+ %code ^Statements\s*:\s*([^%]+)
+ %li
+ excoveralls (Elixir) -
+ %code \[TOTAL\]\s+(\d+\.\d+)%
+
+ = f.submit 'Save changes', class: "btn btn-save"
+
+%hr
+
+.row.prepend-top-default
+ = render partial: 'projects/pipelines_settings/badge', collection: @badges
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
deleted file mode 100644
index 1f698558bce..00000000000
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ /dev/null
@@ -1,98 +0,0 @@
-- page_title "CI/CD Pipelines"
-
-.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = page_title
- .col-lg-9
- = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
- %fieldset.builds-feature
- - unless @repository.gitlab_ci_yml
- .form-group
- %p Pipelines need to be configured before you can begin using Continuous Integration.
- = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
- %hr
- .form-group.append-bottom-default
- = f.label :runners_token, "Runner token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used by the Runner to checkout the project
-
- %hr
- .form-group
- %h5.prepend-top-0
- Git strategy for pipelines
- %p
- Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
- .radio
- = f.label :build_allow_git_fetch_false do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr
- Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr
- Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
-
- %hr
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block
- Per job in minutes. If a job passes this threshold, it will be marked as failed.
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
-
- %hr
- .form-group
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public pipelines
- .help-block
- Allow everyone to access pipelines for public and internal projects
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
-
- %hr
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- A regular expression that will be used to find the test coverage
- output in the build trace. Leave blank to disable
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
- %li
- gcovr (C/C++) -
- %code ^TOTAL.*\s+(\d+\%)$
- %li
- tap --coverage-report=text-summary (NodeJS) -
- %code ^Statements\s*:\s*([^%]+)
- %li
- excoveralls (Elixir) -
- %code \[TOTAL\]\s+(\d+\.\d+)%
-
- = f.submit 'Save changes', class: "btn btn-save"
-
-%hr
-
-.row.prepend-top-default
- = render partial: 'badge', collection: @badges
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 42e9bdbd30e..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_tag('protected_branches/protected_branches_bundle.js')
-
-.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/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 33a9a96183c..2ef1f98ba48 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
.col-sm-10
.checkbox
= f.check_box :active
- %span.light Paused Runners don't accept new builds
+ %span.light Paused Runners don't accept new jobs
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
@@ -32,7 +32,7 @@
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
- = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control'
+ = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
.help-block You can setup jobs to only use Runners with specific tags
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
new file mode 100644
index 00000000000..f9808f7c990
--- /dev/null
+++ b/app/views/projects/runners/_index.html.haml
@@ -0,0 +1,25 @@
+.light.prepend-top-default
+ %p
+ A 'Runner' is a process which runs a job.
+ You can setup as many Runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+
+ %p Each Runner can be in one of the following states:
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ \- Runner is active and can process any new jobs
+ %li
+ %span.label.label-danger paused
+ \- Runner is paused and will not receive any new jobs
+
+%hr
+
+%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners
+.row
+ .col-sm-6
+ = render 'projects/runners/specific_runners'
+ .col-sm-6
+ = render 'projects/runners/shared_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 7036b8a5ccc..deeadb609f6 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -31,6 +31,6 @@
= runner.description
- if runner.tag_list.present?
%p
- - runner.tag_list.each do |tag|
+ - runner.tag_list.sort.each do |tag|
%span.label.label-primary
= tag
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 5afa193357e..0671dd66e78 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -22,7 +22,7 @@
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
- = render partial: 'runner', collection: @shared_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
.light
and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index dcff675eafc..6b8e6bd4fee 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -20,10 +20,10 @@
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @project_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner
- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner
= paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
deleted file mode 100644
index 92957470070..00000000000
--- a/app/views/projects/runners/index.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- page_title "Runners"
-
-.light.prepend-top-default
- %p
- A 'Runner' is a process which runs a build.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
-
- %p Each Runner can be in one of the following states:
- %div
- %ul
- %li
- %span.label.label-success active
- \- Runner is active and can process any new builds
- %li
- %span.label.label-danger paused
- \- Runner is paused and will not receive any new builds
-
-%hr
-
-%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
-.row
- .col-sm-6
- = render 'specific_runners'
- .col-sm-6
- = render 'shared_runners'
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 61b99f35d74..49415ba557b 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -28,7 +28,7 @@
%tr
%td Tags
%td
- - @runner.tag_list.each do |tag|
+ - @runner.tag_list.sort.each do |tag|
%span.label.label-primary
= tag
%tr
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 8ca4c51a064..3a323d94cc2 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
@@ -1,16 +1,19 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
-To setup this service:
-%ul.list-unstyled
+%p To setup this service:
+%ul.list-unstyled.indent-list
%li
1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Enable custom slash commands
+ = icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Mattermost team with these options:
%hr
.help-form
@@ -83,9 +86,14 @@ To setup this service:
%hr
-%ul.list-unstyled
+%ul.list-unstyled.indent-list
%li
- 3. After adding the slash command, paste the
-
- %strong token
+ 3. Paste the
+ %strong Token
into the field below
+ %li
+ 4. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Mattermost!
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 c1e576b42fc..a04fd5035a6 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,13 +1,16 @@
- enabled = Gitlab.config.mattermost.enabled
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Mattermost.
- %br
- See list of available commands in Mattermost after setting up this service,
- by entering
- %code /&lt;command_trigger_word&gt; help
-
+ %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
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
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 04b9100acc6..0d973a20d4c 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,21 +1,25 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
-- run_actions_text = "Perform common operations on this project: #{pretty_name}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Slack.
- %br
- See list of available commands in Slack after setting up this service,
- by entering
- %code /&lt;command&gt; help
- %br
- %br
+ %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
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
- unless @service.template?
- To setup this service:
- %ul.list-unstyled
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
%li
1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
in your Slack team with these options:
%hr
@@ -82,7 +86,7 @@
%hr
- %ul.list-unstyled
+ %ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
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
new file mode 100644
index 00000000000..e2603096014
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -0,0 +1,7 @@
+- page_title "CI/CD Pipelines"
+= render "projects/settings/head"
+
+= render 'projects/runners/index'
+= render 'projects/variables/index'
+= render 'projects/triggers/index'
+= render 'projects/pipelines_settings/show'
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 80d4081dd7b..de1229d58aa 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,15 +1,15 @@
- @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)
@@ -74,8 +74,9 @@
Set up auto deploy
- if @repository.commit
- .project-last-commit{ class: container_class }
- = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
+ %div{ class: container_class }
+ .project-last-commit
+ = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class }
- if @project.archived?
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index e2a5107a883..34ee4ff1937 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
@@ -8,7 +10,7 @@
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
- - if @snippet.submittable_as_spam? && current_user.admin?
+ - if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
@@ -29,6 +31,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit
- - if @snippet.submittable_as_spam? && current_user.admin?
+ - if @snippet.submittable_as_spam_by?(current_user)
%li
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 485b23815bc..6b3d7d4008b 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,7 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/projects/snippets/verify.html.haml b/app/views/projects/snippets/verify.html.haml
new file mode 100644
index 00000000000..eb56f03b3f4
--- /dev/null
+++ b/app/views/projects/snippets/verify.html.haml
@@ -0,0 +1,4 @@
+- form = [@project.namespace.becomes(Namespace), @project, @snippet.becomes(Snippet)]
+
+= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+
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/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index a1f4e3e8ed6..bdcc160a067 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
%article.file-holder.readme-holder
- .file-title
+ .js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
%strong
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
new file mode 100644
index 00000000000..cc74e50a5e3
--- /dev/null
+++ b/app/views/projects/triggers/_index.html.haml
@@ -0,0 +1,104 @@
+.row.prepend-top-default.append-bottom-default.triggers-container
+ .col-lg-3
+ = 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.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 form above.
+
+ .panel-footer
+
+ %p
+ In the following examples, you can see the exact API call you need to
+ make in order to rebuild a specific
+ %code ref
+ (branch or tag) with a trigger token.
+ %p
+ All you need to do is replace the
+ %code TOKEN
+ and
+ %code REF_NAME
+ with the trigger token and the branch or tag name respectively.
+
+ %h5.prepend-top-default
+ Use cURL
+
+ %p.light
+ Copy one of the tokens above, set your branch or tag name, and that
+ reference will be rebuilt.
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F ref=REF_NAME \
+ #{builds_trigger_url(@project.id)}
+ %h5.prepend-top-default
+ Use .gitlab-ci.yml
+
+ %p.light
+ In the
+ %code .gitlab-ci.yml
+ of another project, include the following snippet.
+ The project will be rebuilt at the end of the pipeline.
+
+ %pre
+ :plain
+ trigger_build:
+ stage: deploy
+ script:
+ - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
+ %h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
+ Pass job variables
+
+ %p.light
+ Add
+ %code variables[VARIABLE]=VALUE
+ to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines.
+
+ With cURL:
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "ref=REF_NAME" \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
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/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
deleted file mode 100644
index 6e5dd1b196d..00000000000
--- a/app/views/projects/triggers/index.html.haml
+++ /dev/null
@@ -1,110 +0,0 @@
-- page_title "Triggers"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
- = page_title
- %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'
- .col-lg-9
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- Manage your project's triggers
- .panel-body
- - if @triggers.any?
- .table-responsive
- %table.table
- %thead
- %th
- %strong Token
- %th
- %strong Last used
- %th
- = render partial: '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'
-
- .panel-footer
-
- %p
- In the following examples, you can see the exact API call you need to
- make in order to rebuild a specific
- %code ref
- (branch or tag) with a trigger token.
- %p
- All you need to do is replace the
- %code TOKEN
- and
- %code REF_NAME
- with the trigger token and the branch or tag name respectively.
-
- %h5.prepend-top-default
- Use cURL
-
- %p.light
- Copy one of the tokens above, set your branch or tag name, and that
- reference will be rebuilt.
-
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F ref=REF_NAME \
- #{builds_trigger_url(@project.id)}
- %h5.prepend-top-default
- Use .gitlab-ci.yml
-
- %p.light
- In the
- %code .gitlab-ci.yml
- of another project, include the following snippet.
- The project will be rebuilt at the end of the build.
-
- %pre
- :plain
- trigger_build:
- stage: deploy
- script:
- - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
- %h5.prepend-top-default
- Use webhook
-
- %p.light
- Add the following webhook to another project for Push and Tag push events.
- The project will be rebuilt at the corresponding event.
-
- %pre
- :plain
- #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
- %h5.prepend-top-default
- Pass build variables
-
- %p.light
- Add
- %code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
-
- With cURL:
-
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F "ref=REF_NAME" \
- -F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{builds_trigger_url(@project.id)}
- %p.light
- With webhook:
-
- %pre.append-bottom-0
- :plain
- #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
index 0249e0c1bf1..06477aba103 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/projects/variables/_content.html.haml
@@ -5,4 +5,4 @@
%p
So you can use them for passwords, secret keys or whatever you want.
%p
- The value of the variable can be visible in build log if explicitly asked to do so.
+ The value of the variable can be visible in job log if explicitly asked to do so.
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml
index a5bae83e0ce..1ae86d258af 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/projects/variables/_form.html.haml
@@ -6,5 +6,5 @@
= f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
.form-group
= f.label :value, "Value", class: "label-light"
- = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+ = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
= f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/variables/_index.html.haml b/app/views/projects/variables/_index.html.haml
new file mode 100644
index 00000000000..1b852a9c5b3
--- /dev/null
+++ b/app/views/projects/variables/_index.html.haml
@@ -0,0 +1,16 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "projects/variables/content"
+ .col-lg-9
+ %h5.prepend-top-0
+ Add a variable
+ = render "projects/variables/form", btn_text: "Add new variable"
+ %hr
+ %h5.prepend-top-0
+ Your variables (#{@project.variables.size})
+ - if @project.variables.empty?
+ %p.settings-message.text-center.append-bottom-0
+ No variables found, add one with the form above.
+ - else
+ = render "projects/variables/table"
+ %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml
deleted file mode 100644
index cf7ae0b489f..00000000000
--- a/app/views/projects/variables/index.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- page_title "Variables"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- = render "content"
- .col-lg-9
- %h5.prepend-top-0
- Add a variable
- = render "form", btn_text: "Add new variable"
- %hr
- %h5.prepend-top-0
- Your variables (#{@project.variables.size})
- - if @project.variables.empty?
- %p.settings-message.text-center.append-bottom-0
- No variables found, add one with the form above.
- - else
- = render "table"
- %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index c74f53b4c39..3d33679f07d 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -13,5 +13,9 @@
= label_tag :new_wiki_path do
%span Page slug
= text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ %span.new-wiki-page-slug-tip
+ = icon('lightbulb-o')
+ Tip: You can specify the full path for the new file.
+ We will automatically create any missing directories.
.form-actions
= button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
new file mode 100644
index 00000000000..6298cf6c8da
--- /dev/null
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -0,0 +1,5 @@
+%li
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index cad9c15a49e..8c582f747b3 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
@@ -12,10 +12,8 @@
.blocks-container
.block.block-first
%ul.wiki-pages
- - @sidebar_wiki_pages.each do |wiki_page|
- %li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
- = wiki_page.title.capitalize
+ = render @sidebar_wiki_entries, context: 'sidebar'
+
.block
= link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do
More Pages
diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
new file mode 100644
index 00000000000..0a61d90177b
--- /dev/null
+++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
@@ -0,0 +1,3 @@
+%li{ class: active_when(params[:id] == wiki_page.slug) }
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
+ = wiki_page.title.capitalize
diff --git a/app/views/projects/wikis/_wiki_directory.html.haml b/app/views/projects/wikis/_wiki_directory.html.haml
new file mode 100644
index 00000000000..0e5f32ed859
--- /dev/null
+++ b/app/views/projects/wikis/_wiki_directory.html.haml
@@ -0,0 +1,4 @@
+%li
+ = wiki_directory.slug
+ %ul
+ = render wiki_directory.pages, context: context
diff --git a/app/views/projects/wikis/_wiki_page.html.haml b/app/views/projects/wikis/_wiki_page.html.haml
new file mode 100644
index 00000000000..c84d06dad02
--- /dev/null
+++ b/app/views/projects/wikis/_wiki_page.html.haml
@@ -0,0 +1 @@
+= render "#{context}_wiki_page", wiki_page: wiki_page
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index e1eaffc6884..5fba2b1a5ae 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -13,11 +13,7 @@
= icon('cloud-download')
Clone repository
- %ul.content-list
- - @wiki_pages.each do |wiki_page|
- %li
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ %ul.wiki-pages-list.content-list
+ = render @wiki_entries, context: 'pages'
+
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 1b6dceee241..3609461b721 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -6,9 +6,11 @@
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
+ .wiki-breadcrumb
+ %span= breadcrumb(@page.slug)
+
.nav-text
%h2.wiki-page-title= @page.title.capitalize
-
%span.wiki-last-edit-by
Last edited by
%strong
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 8cbecb725b5..5afb95ac430 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,70 +1,70 @@
%ul.nav-links.search-filter
- if @project
- %li{ class: ("active" if @scope == 'blobs') }
+ %li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
%span.badge
= @search_results.blobs_count
- %li{ class: ("active" if @scope == 'issues') }
+ %li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{ class: ("active" if @scope == 'merge_requests') }
+ %li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{ class: ("active" if @scope == 'milestones') }
+ %li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
= @search_results.milestones_count
- %li{ class: ("active" if @scope == 'notes') }
+ %li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
%span.badge
= @search_results.notes_count
- %li{ class: ("active" if @scope == 'wiki_blobs') }
+ %li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
%span.badge
= @search_results.wiki_blobs_count
- %li{ class: ("active" if @scope == 'commits') }
+ %li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
%span.badge
= @search_results.commits_count
- elsif @show_snippets
- %li{ class: ("active" if @scope == 'snippet_blobs') }
+ %li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents
%span.badge
= @search_results.snippet_blobs_count
- %li{ class: ("active" if @scope == 'snippet_titles') }
+ %li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
- else
- %li{ class: ("active" if @scope == 'projects') }
+ %li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
Projects
%span.badge
= @search_results.projects_count
- %li{ class: ("active" if @scope == 'issues') }
+ %li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{ class: ("active" if @scope == 'merge_requests') }
+ %li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{ class: ("active" if @scope == 'milestones') }
+ %li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 7fe2bce3e7c..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.list-unstyled
+ %ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects
- else
.search-results
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 9e8adc82583..7f1f807e2e7 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,7 +1,7 @@
- file_name, blob = blob
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
- ref = @search_results.repository_ref
- blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 23ca6479414..f84be600df8 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -7,46 +7,39 @@
= snippet.title
by
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
+ = image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet)
- = link_to snippet_path do
- .file-holder
- .file-title
+ .file-holder
+ .js-file-title.file-title
+ = link_to snippet_path do
%i.fa.fa-file
%strong= snippet.file_name
- - if markup?(snippet.file_name)
- .file-content.wiki
+ - if markup?(snippet.file_name)
+ .file-content.wiki
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = render_markup(snippet.file_name, chunk[:data])
+ - else
+ .file-content.code
+ .nothing-here-block Empty file
+ - else
+ .file-content.code.js-syntax-highlight
+ .line-numbers
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = render_markup(snippet.file_name, chunk[:data])
+ - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
+ - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
+ - i = index + offset
+ = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
+ %i.fa.fa-link
+ = i
+ .blob-content
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?)
- else
.file-content.code
.nothing-here-block Empty file
- - else
- .file-content.code.js-syntax-highlight
- .line-numbers
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- - i = index + offset
- = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
- %i.fa.fa-link
- = i
- - unless snippet == snippet_chunks.last
- %a.diff-line-num
- = "."
- %pre.code
- %code
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- = chunk[:data]
- - unless chunk == snippet_chunks.last
- %a
- = "..."
- - else
- .file-content.code
- .nothing-here-block Empty file
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 704d1d01a81..026f404ce07 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -18,6 +18,6 @@
%span
by
= link_to user_snippets_path(snippet_title.author) do
- = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
+ = image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 648d0bd76cb..d87f9df2677 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,7 +1,7 @@
- wiki_blob = parse_search_result(wiki_blob)
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
= link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
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/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index c196bc06b17..4b98ff88241 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -17,9 +17,9 @@
Try to keep the first line under 52 characters
and the others under 72.
- if descriptions.present?
- %p.hint.js-with-description-hint
+ .hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do
Include description in commit message
- %p.hint.js-without-description-hint.hide
+ .hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do
Don't include description in commit message
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 0bc851b4256..c2d9ac87b20 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,3 +1,4 @@
+- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,11 +12,16 @@
.col-sm-10
.input-group.gl-field-error-anchor
.input-group-addon
- = root_url
+ %span>= root_url
+ - if parent
+ %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
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
new file mode 100644
index 00000000000..1d4fd71522d
--- /dev/null
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -0,0 +1,25 @@
+- note_count = @issuable_meta_data[issuable.id].notes_count
+- 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
+ = icon('thumbs-up')
+ = upvotes
+
+- if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
+%li
+ = link_to issuable_url, class: ('no-comments' if note_count.zero?) do
+ = icon('comments')
+ = note_count
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index ead9b84b991..bd994cdad01 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,4 @@
- label_css_id = dom_id(label)
-- open_issues_count = label.open_issues_count(current_user)
-- open_merge_requests_count = label.open_merge_requests_count(current_user)
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
@@ -15,10 +13,10 @@
%ul
%li
= link_to_label(label, subject: subject, type: :merge_request) do
- = pluralize open_merge_requests_count, 'merge request'
+ view merge requests
%li
= link_to_label(label, subject: subject) do
- = pluralize open_issues_count, 'open issue'
+ view open issues
- if current_user && defined?(@project)
%li.label-subscription
- if label.is_a?(ProjectLabel)
@@ -40,18 +38,18 @@
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
- = pluralize open_merge_requests_count, 'merge request'
+ view merge requests
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
- = pluralize open_issues_count, 'open issue'
+ view open issues
- 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/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
new file mode 100644
index 00000000000..b0778653d4e
--- /dev/null
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -0,0 +1,18 @@
+.stage-cell
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown{ class: klass }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
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/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index b11257ee0e6..73efec88bb1 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,8 +1,11 @@
+- with_label = local_assigns.fetch(:with_label, true)
+
.form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'control-label' do
- Visibility Level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- .col-sm-10
+ - if with_label
+ = f.label :visibility_level, class: 'control-label' do
+ Visibility Level
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ %div{ :class => ("col-sm-10" if with_label) }
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index b6047ece592..3baa956b910 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
%ul.nav-links
- %li{ class: ('active' if scope.nil?) }>
+ %li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id))
- %li{ class: ('active' if scope == 'pending') }>
+ %li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
= number_with_delimiter(all_builds.pending.count(:id))
- %li{ class: ('active' if scope == 'running') }>
+ %li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
= number_with_delimiter(all_builds.running.count(:id))
- %li{ class: ('active' if scope == 'finished') }>
+ %li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
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 dd9e433491b..60ca23ef680 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,4 +1,5 @@
- group_member = local_assigns[:group_member]
+- full_name = true unless local_assigns[:full_name] == false
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
@@ -28,7 +29,10 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.full_name
+ - if full_name
+ = group.full_name
+ - else
+ = 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 b42eaabb111..f17ae9f28eb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -38,8 +38,9 @@
#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.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) } }
+ %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" }
@@ -53,7 +54,7 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
@@ -61,13 +62,13 @@
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
- = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
@@ -91,5 +92,5 @@
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0a4de709fcd..0b0f2c9cd1a 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -43,41 +43,49 @@
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
-- if @merge_request_for_resolving_discussions
+= render 'shared/issuable/form/merge_params', issuable: issuable
+
+- 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/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 1154316c03f..ad995cbe962 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
- %li{ class: ("active" if params[:state] == 'opened') }>
+ %li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
- %li{ class: ("active" if params[:state] == 'merged') }>
+ %li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)}
- %li{ class: ("active" if params[:state] == 'closed') }>
+ %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)}
- else
- %li{ class: ("active" if params[:state] == 'closed') }>
+ %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)}
- %li{ class: ("active" if params[:state] == 'all') }>
+ %li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 55360dadbc4..f8123846596 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,18 +11,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]) }
- = 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', '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
@@ -32,57 +35,57 @@
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
- #js-dropdown-author.dropdown-menu
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
+ %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
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
+ %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-dropdown' => true }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
+ %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.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-dropdown' => true }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
+ %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
+ .pull-right.filter-dropdown-container
= render 'shared/sort_dropdown'
- if @bulk_edit
@@ -101,7 +104,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
@@ -112,7 +115,7 @@
= 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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 10fa7901874..048fc488207 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,23 +1,25 @@
- todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('issuable/issuable_bundle.js')
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+ = 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' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
- 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
@@ -25,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: 'block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -36,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
@@ -53,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' } }
@@ -62,7 +64,7 @@
None
.title.hide-collapsed
Milestone
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -76,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: '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
@@ -109,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
@@ -119,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', 'aria-hidden': 'true')
%span
= selected_labels.size
.title.hide-collapsed
Labels
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: '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?) }
@@ -140,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
@@ -151,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'
@@ -172,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/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index b757893ea04..2793e7bcff4 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -19,12 +19,3 @@
- if issuable.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(issuable)
-
-- if issuable.can_remove_source_branch?(current_user)
- .form-group
- .col-sm-10.col-sm-offset-2
- .checkbox
- = label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
- Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
new file mode 100644
index 00000000000..03309722326
--- /dev/null
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -0,0 +1,16 @@
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless issuable.is_a?(MergeRequest)
+- return if issuable.closed_without_fork?
+
+-# This check is duplicated below, to avoid conflicts with EE.
+- return unless issuable.can_remove_source_branch?(current_user)
+
+.form-group
+ .col-sm-10.col-sm-offset-2
+ - if issuable.can_remove_source_branch?(current_user)
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index a47085230b8..7a21f19ded4 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -13,10 +13,10 @@
= 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) }
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 81b5bc1de30..1d5a61cffce 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -6,7 +6,7 @@
%span.list-item-name
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
- = link_to group.name, group_path(group)
+ = link_to group.full_name, group_path(group)
.cgray
Joined #{time_ago_with_tooltip(group.created_at)}
- if group_link.expires?
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 659d4c905fc..8e721c9c8dd 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -19,9 +19,9 @@
%label.label.label-danger
%strong Blocked
- - if source.instance_of?(Group) && !@group
+ - if source.instance_of?(Group) && source != @group
&middot;
- = link_to source.name, source, class: "member-group-link"
+ = link_to source.full_name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -44,8 +44,9 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if show_roles
+ - current_resource = @project || @group
.controls.member-controls
- - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
+ - if show_controls && member.source == current_resource
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
@@ -60,7 +61,7 @@
= dropdown_title("Change permissions")
.dropdown-content
%ul
- - Gitlab::Access.options.each do |role, role_id|
+ - member.class.access_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 748b10a1298..ed94773ef89 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -10,6 +10,3 @@
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
-
-:javascript
- new gl.DueDateSelectors();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 28935c8b598..4c7d69d40d5 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -5,7 +5,7 @@
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
-%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
%span
- if show_project_name
%strong #{project.name} &middot;
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 31eb07ca666..8af3bd597c5 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -3,16 +3,16 @@
- panel_class = primary ? 'panel-primary' : 'panel-default'
.panel{ class: panel_class }
- .panel-heading.split
- .left
+ .panel-heading
+ .title
= title
- if show_counter
- .right
+ .counter
= number_with_delimiter(issuables.size)
- 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 b7f8551153b..2d25b8aad62 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,5 @@
- @sort ||= sort_value_recently_updated
-- personal = params[:personal]
-- archived = params[:archived]
-- namespace_id = params[:namespace_id]
-.dropdown.inline
+.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
@@ -10,21 +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(shared: nil), class: ("is-active" unless params[:shared].present?) do
+ All projects
+ %li
+ = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
+ Hide shared projects
+ %li
+ = 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/_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/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 2d22782eb36..e7f7db73223 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('snippet/snippet_bundle.js')
+ = 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|
@@ -18,7 +18,7 @@
= f.label :file_name, "File", class: 'control-label'
.col-sm-10
.file-holder.snippet
- .file-title
+ .js-file-title.file-title
= f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name'
.file-content.code
%pre#editor= @snippet.content
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 13586a5a12a..37e2a377a69 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -3,7 +3,7 @@
%h4.prepend-top-0
= page_title
%p
- #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be
+ #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
@@ -66,9 +66,9 @@
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
- %strong Build events
+ %strong Jobs events
%p.light
- This URL will be triggered when the build status changes
+ This URL will be triggered when the job status changes
%li
= f.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 92151176fce..1a6e2542dc1 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -26,7 +26,7 @@
= @file_sample.events
%article.file-holder
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file-text-o.fa-fw
%strong
= @file_sample.file
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 9a9a3ff9220..a7f118d3f7d 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
@@ -5,29 +7,27 @@
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
- New snippet
- - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
+ - if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
-- if current_user
- .visible-xs-block.dropdown
- %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-full-width
- %ul
+.visible-xs-block.dropdown
+ %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-full-width
+ %ul
+ %li
+ = link_to new_snippet_path, title: "New snippet" do
+ New snippet
+ - if can?(current_user, :admin_personal_snippet, @snippet)
%li
- = link_to new_snippet_path, title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- %li
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
- - if can?(current_user, :update_personal_snippet, @snippet)
- %li
- = link_to edit_snippet_path(@snippet) do
- Edit
- - if @snippet.submittable_as_spam? && current_user.admin?
- %li
- = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
+ - if @snippet.submittable_as_spam_by?(current_user)
+ %li
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index 2dda5fed647..8b6a98a054a 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -2,7 +2,7 @@
- include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
+ %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
%span.badge
@@ -12,19 +12,19 @@
= subject.snippets.public_and_internal.count
- if include_private
- %li{ class: ("active" if params[:scope] == "are_private") }
+ %li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
%span.badge
= subject.snippets.are_private.count
- %li{ class: ("active" if params[:scope] == "are_internal") }
+ %li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
%span.badge
= subject.snippets.are_internal.count
- %li{ class: ("active" if params[:scope] == "are_public") }
+ %li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
%span.badge
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 837a1a0cc8c..970afbe6b64 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,7 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/snippets/verify.html.haml b/app/views/snippets/verify.html.haml
new file mode 100644
index 00000000000..cb623ccab57
--- /dev/null
+++ b/app/views/snippets/verify.html.haml
@@ -0,0 +1,4 @@
+- form = [@snippet.becomes(Snippet)]
+
+= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 6228245d8d0..57b8845c55d 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,7 +1,7 @@
.clearfix.calendar
.js-contrib-calendar
.calendar-hint
- Summary of issues, merge requests, and push events
+ Summary of issues, merge requests, push events, and comments
:javascript
new Calendar(
#{@activity_dates.to_json},
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index b09782749f5..4afd31f788b 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -10,11 +10,17 @@
%i.fa.fa-clock-o
= event.created_at.to_s(:time)
- if event.push?
- #{event.action_name} #{event.ref_type} #{event.ref_name}
+ #{event.action_name} #{event.ref_type}
+ %strong
+ - commits_path = namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
- else
= event_action_name(event)
- - if event.target
- %strong= link_to "#{event.target.to_reference}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong
+ - if event.note?
+ = link_to event.note_target.to_reference, event_note_target_path(event)
+ - elsif event.target
+ = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
at
%strong
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c3d33d49c1e..76cd330e80a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,8 +1,8 @@
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/d3.js')
- = page_specific_javascript_tag('users/users_bundle.js')
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -24,13 +24,12 @@
= 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
@@ -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
@@ -106,6 +106,8 @@
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
+ %h4.prepend-top-20
+ Most Recent Activity
.content_list{ data: { href: user_path } }
= spinner
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 6abbb5a5250..13207a8bc71 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -10,12 +10,12 @@ 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)
user = User.find_by(id: user_id)
- user.refresh_authorized_projects if user
+ user&.refresh_authorized_projects
end
end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 3194c389b3d..3340a7be4fe 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -6,6 +6,8 @@ class DeleteUserWorker
delete_user = User.find(delete_user_id)
current_user = User.find(current_user_id)
- DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
+ rescue Gitlab::Access::AccessDeniedError => e
+ Rails.logger.warn("User could not be destroyed: #{e}")
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index b9cd49985dc..f5ccc84c160 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,15 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new(project, after_sha)
+ .execute(project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new(project, before_sha)
+ .execute(project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index a49a5fd0855..07e82767b06 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -11,6 +11,6 @@ class GroupDestroyWorker
user = User.find(user_id)
- DestroyGroupService.new(group, user).execute
+ Groups::DestroyService.new(group, user).execute
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/pages_worker.rb b/app/workers/pages_worker.rb
new file mode 100644
index 00000000000..4eeb9666bb0
--- /dev/null
+++ b/app/workers/pages_worker.rb
@@ -0,0 +1,23 @@
+class PagesWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :pages, retry: false
+
+ def perform(action, *arg)
+ send(action, *arg)
+ end
+
+ def deploy(build_id)
+ build = Ci::Build.find_by(id: build_id)
+ result = Projects::UpdatePagesService.new(build.project, build).execute
+ if result[:status] == :success
+ result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+ end
+ result
+ end
+
+ def remove(namespace_path, project_path)
+ full_path = File.join(Settings.pages.path, namespace_path, project_path)
+ FileUtils.rm_r(full_path, force: true)
+ end
+end
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/bin/teaspoon b/bin/teaspoon
deleted file mode 100755
index 7c3b8dfc4ed..00000000000
--- a/bin/teaspoon
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env ruby
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require 'bundler/setup'
-load Gem.bin_path('teaspoon', 'teaspoon')
diff --git a/changelogs/unreleased/1051-api-create-users-without-password.yml b/changelogs/unreleased/1051-api-create-users-without-password.yml
new file mode 100644
index 00000000000..24b5a73b45c
--- /dev/null
+++ b/changelogs/unreleased/1051-api-create-users-without-password.yml
@@ -0,0 +1,4 @@
+---
+title: Optionally make users created via the API set their password
+merge_request: 8957
+author: Joost Rijneveld
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/1363-redo-mailroom-support.yml b/changelogs/unreleased/1363-redo-mailroom-support.yml
new file mode 100644
index 00000000000..8ed206f4fdb
--- /dev/null
+++ b/changelogs/unreleased/1363-redo-mailroom-support.yml
@@ -0,0 +1,4 @@
+---
+title: Redo internals of Incoming Mail Support
+merge_request: 9385
+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/14492-change-fork-endpoint.yml b/changelogs/unreleased/14492-change-fork-endpoint.yml
new file mode 100644
index 00000000000..39024b51b54
--- /dev/null
+++ b/changelogs/unreleased/14492-change-fork-endpoint.yml
@@ -0,0 +1,4 @@
+---
+title: Move /projects/fork/:id to /projects/:id/fork
+merge_request: 8940
+author:
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/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/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml
deleted file mode 100644
index c26a20f87e2..00000000000
--- a/changelogs/unreleased/19164-mobile-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 19164 Add settings dropdown to mobile screens
-merge_request:
-author:
diff --git a/changelogs/unreleased/19302-wiki-page-delete-does-not-trigger-the-webhook.yml b/changelogs/unreleased/19302-wiki-page-delete-does-not-trigger-the-webhook.yml
new file mode 100644
index 00000000000..d74057dca8a
--- /dev/null
+++ b/changelogs/unreleased/19302-wiki-page-delete-does-not-trigger-the-webhook.yml
@@ -0,0 +1,4 @@
+---
+title: Execute web hooks for WikiPage delete operation
+merge_request: 8198
+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/20495-plus-icon-button.yml b/changelogs/unreleased/20495-plus-icon-button.yml
new file mode 100644
index 00000000000..0f8650eb7b6
--- /dev/null
+++ b/changelogs/unreleased/20495-plus-icon-button.yml
@@ -0,0 +1,4 @@
+---
+title: Remove plus icon from MR button on compare view
+merge_request:
+author:
diff --git a/changelogs/unreleased/20732_member_exists_409.yml b/changelogs/unreleased/20732_member_exists_409.yml
new file mode 100644
index 00000000000..135647c7ac3
--- /dev/null
+++ b/changelogs/unreleased/20732_member_exists_409.yml
@@ -0,0 +1,4 @@
+---
+title: 'Add member: Always return 409 when a member exists'
+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/21240_snippets_line_ending.yml b/changelogs/unreleased/21240_snippets_line_ending.yml
new file mode 100644
index 00000000000..880fdd2c9ed
--- /dev/null
+++ b/changelogs/unreleased/21240_snippets_line_ending.yml
@@ -0,0 +1,4 @@
+---
+title: Download snippets with LF line-endings by default
+merge_request: 8999
+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/22018-api-milestone-merge-requests.yml b/changelogs/unreleased/22018-api-milestone-merge-requests.yml
new file mode 100644
index 00000000000..ccad2ec838c
--- /dev/null
+++ b/changelogs/unreleased/22018-api-milestone-merge-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Adds API endpoint to fetch all merge request for a single milestone
+merge_request:
+author: Joren De Groof
diff --git a/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml b/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml
new file mode 100644
index 00000000000..028923b83cf
--- /dev/null
+++ b/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Standardize branch name params as branch on V4 API
+merge_request: 8936
+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/22645-add-discussion-contribs-to-calendar.yml b/changelogs/unreleased/22645-add-discussion-contribs-to-calendar.yml
new file mode 100644
index 00000000000..9b3c2bd9278
--- /dev/null
+++ b/changelogs/unreleased/22645-add-discussion-contribs-to-calendar.yml
@@ -0,0 +1,4 @@
+---
+title: Add discussion events to contributions calendar
+merge_request: 8821
+author:
diff --git a/changelogs/unreleased/22818-licence-gitignore-and-yml-endpoints-removal.yml b/changelogs/unreleased/22818-licence-gitignore-and-yml-endpoints-removal.yml
new file mode 100644
index 00000000000..05d5993ddf3
--- /dev/null
+++ b/changelogs/unreleased/22818-licence-gitignore-and-yml-endpoints-removal.yml
@@ -0,0 +1,4 @@
+---
+title: V3 deprecated templates endpoints removal
+merge_request: 8853
+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/23061-consolidate-project-lists.yml b/changelogs/unreleased/23061-consolidate-project-lists.yml
new file mode 100644
index 00000000000..dbb8fed55c0
--- /dev/null
+++ b/changelogs/unreleased/23061-consolidate-project-lists.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Consolidate /projects endpoint'
+merge_request: 8962
+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/23104-remove-public-param-for-projects.yml b/changelogs/unreleased/23104-remove-public-param-for-projects.yml
new file mode 100644
index 00000000000..78eb785279f
--- /dev/null
+++ b/changelogs/unreleased/23104-remove-public-param-for-projects.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: remove `public` param for projects'
+merge_request: 8736
+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/23535-folders-in-wiki-repository.yml b/changelogs/unreleased/23535-folders-in-wiki-repository.yml
new file mode 100644
index 00000000000..05212b608d4
--- /dev/null
+++ b/changelogs/unreleased/23535-folders-in-wiki-repository.yml
@@ -0,0 +1,4 @@
+---
+title: Show directory hierarchy when listing wiki pages
+merge_request: 8133
+author: Alex Braha Stoll
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/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml
new file mode 100644
index 00000000000..628db8a5419
--- /dev/null
+++ b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml
@@ -0,0 +1,4 @@
+---
+title: Fix position of counter in milestone panels
+merge_request: 7842
+author: Andrew Smith (EspadaV8)
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/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/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/24333-close-issues-with-merge-request-title-ui.yml b/changelogs/unreleased/24333-close-issues-with-merge-request-title-ui.yml
new file mode 100644
index 00000000000..fa137a29cb4
--- /dev/null
+++ b/changelogs/unreleased/24333-close-issues-with-merge-request-title-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Show Issues mentioned / being closed from a Merge Requests title below the
+ 'Accept Merge Request' button
+merge_request: 9194
+author: Jan Christophersen
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/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
deleted file mode 100644
index 05fbd8f0bf2..00000000000
--- a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
-merge_request: 8752
-author:
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/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/24976-start-of-line-mention.yml b/changelogs/unreleased/24976-start-of-line-mention.yml
new file mode 100644
index 00000000000..99208aac87c
--- /dev/null
+++ b/changelogs/unreleased/24976-start-of-line-mention.yml
@@ -0,0 +1,4 @@
+---
+title: Added a feature to create a 'directly addressed' Todo when mentioned in the beginning of a line.
+merge_request: 7926
+author: Ershad Kunnakkadan
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/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/25465-todo-done-clicking-is-kind-of-unsafe.yml b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml
new file mode 100644
index 00000000000..e9d46f6b122
--- /dev/null
+++ b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml
@@ -0,0 +1,4 @@
+---
+title: Todo done clicking is kind of unusable
+merge_request: 8691
+author: Jacopo Beschi @jacopo-beschi
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/25709-diff-file-overflow.yml b/changelogs/unreleased/25709-diff-file-overflow.yml
new file mode 100644
index 00000000000..7d1b2b36ab8
--- /dev/null
+++ b/changelogs/unreleased/25709-diff-file-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Responsive title in diffs inline, side by side, with and without sidebar
+merge_request: 8475
+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/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/26087-asciidoc-cicd-badges-snippet.yml b/changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.yml
new file mode 100644
index 00000000000..799c5277207
--- /dev/null
+++ b/changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.yml
@@ -0,0 +1,4 @@
+---
+title: Added AsciiDoc Snippet to CI/CD Badges
+merge_request: 9164
+author: Jan Christophersen
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/26206-fix-download-dropdown.yml b/changelogs/unreleased/26206-fix-download-dropdown.yml
new file mode 100644
index 00000000000..a6c101375bb
--- /dev/null
+++ b/changelogs/unreleased/26206-fix-download-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Set dropdown height fixed to 250px and make it scrollable
+merge_request: 9063
+author:
diff --git a/changelogs/unreleased/26286-most-recent-activity-profile-header.yml b/changelogs/unreleased/26286-most-recent-activity-profile-header.yml
new file mode 100644
index 00000000000..74d5a43a804
--- /dev/null
+++ b/changelogs/unreleased/26286-most-recent-activity-profile-header.yml
@@ -0,0 +1,4 @@
+---
+title: Added 'Most Recent Activity' header to the User Profile page
+merge_request: 9189
+author: Jan Christophersen
diff --git a/changelogs/unreleased/26287-link-branch-in-calendar-activity.yml b/changelogs/unreleased/26287-link-branch-in-calendar-activity.yml
new file mode 100644
index 00000000000..35855578d21
--- /dev/null
+++ b/changelogs/unreleased/26287-link-branch-in-calendar-activity.yml
@@ -0,0 +1,4 @@
+---
+title: Add Links to Branches in Calendar Activity
+merge_request: 9224
+author: Jan Christophersen
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/26315-unify-labels-filter-behavior.yml b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml
new file mode 100644
index 00000000000..cd2f40c94fe
--- /dev/null
+++ b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml
@@ -0,0 +1,4 @@
+---
+title: Unify issues search behavior by always filtering when ALL labels matches
+merge_request: 8849
+author:
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/26379-iid-param.yml b/changelogs/unreleased/26379-iid-param.yml
new file mode 100644
index 00000000000..ac743e68d6f
--- /dev/null
+++ b/changelogs/unreleased/26379-iid-param.yml
@@ -0,0 +1,4 @@
+---
+title: add :iids param to IssuableFinder (resolve technical dept)
+merge_request: 9222
+author: mhasbini
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/26500-informative-slack-notifications.yml b/changelogs/unreleased/26500-informative-slack-notifications.yml
new file mode 100644
index 00000000000..342235424f4
--- /dev/null
+++ b/changelogs/unreleased/26500-informative-slack-notifications.yml
@@ -0,0 +1,4 @@
+---
+title: Add user & build links in Slack Notifications
+merge_request: 8641
+author: Poornima M
diff --git a/changelogs/unreleased/26651-cannot-move-project-into-group.yml b/changelogs/unreleased/26651-cannot-move-project-into-group.yml
new file mode 100644
index 00000000000..244a19a627d
--- /dev/null
+++ b/changelogs/unreleased/26651-cannot-move-project-into-group.yml
@@ -0,0 +1,4 @@
+---
+title: Specify in the documentation that only projects owners can transfer projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/26703-todos-count.yml b/changelogs/unreleased/26703-todos-count.yml
new file mode 100644
index 00000000000..24fd0c406e2
--- /dev/null
+++ b/changelogs/unreleased/26703-todos-count.yml
@@ -0,0 +1,4 @@
+---
+title: show 99+ for large count in todos notification bell
+merge_request: 9171
+author: mhasbini
diff --git a/changelogs/unreleased/26705-filter-todos-by-manual-add.yml b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml
new file mode 100644
index 00000000000..3521496a20e
--- /dev/null
+++ b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml
@@ -0,0 +1,4 @@
+---
+title: Filter todos by manual add
+merge_request: 8691
+author: Jacopo Beschi @jacopo-beschi
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/26744-add-omniauth-oauth2-generic-strategy.yml b/changelogs/unreleased/26744-add-omniauth-oauth2-generic-strategy.yml
new file mode 100644
index 00000000000..15da43b8091
--- /dev/null
+++ b/changelogs/unreleased/26744-add-omniauth-oauth2-generic-strategy.yml
@@ -0,0 +1,3 @@
+title: Add the oauth2_generic OmniAuth strategy
+merge_request: 9048
+author: Joe Marty \ No newline at end of file
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/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/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/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/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
new file mode 100644
index 00000000000..0e8f7093b34
--- /dev/null
+++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
@@ -0,0 +1,4 @@
+---
+title: Refactor Timelogs structure to use foreign keys.
+merge_request: 8769
+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/26957-tanuki-anim-hang.yml b/changelogs/unreleased/26957-tanuki-anim-hang.yml
new file mode 100644
index 00000000000..c7b4b9ebdfd
--- /dev/null
+++ b/changelogs/unreleased/26957-tanuki-anim-hang.yml
@@ -0,0 +1,4 @@
+---
+title: don't animate logo when downloading files
+merge_request:
+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/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml
deleted file mode 100644
index 1758ed9e9ea..00000000000
--- a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support non-ASCII characters in GFM autocomplete
-merge_request: 8729
-author:
diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml
deleted file mode 100644
index ddd454da376..00000000000
--- a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix permalink discussion note being collapsed
-merge_request:
-author:
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/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/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml b/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml
deleted file mode 100644
index 6e036923158..00000000000
--- a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix filtering with multiple words
-merge_request: 8830
-author:
diff --git a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml b/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml
deleted file mode 100644
index 2591f161bc5..00000000000
--- a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project name label's for reference in project settings
-merge_request: 8795
-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/27287-label-dropdown-error-messages.yml b/changelogs/unreleased/27287-label-dropdown-error-messages.yml
new file mode 100644
index 00000000000..dfd4102c324
--- /dev/null
+++ b/changelogs/unreleased/27287-label-dropdown-error-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Fix displaying error messages for create label dropdown
+merge_request: 9058
+author: Tom Koole
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/27336-add-environment-url-link-to-terminal-page.yml b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml
new file mode 100644
index 00000000000..dd4907166c4
--- /dev/null
+++ b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml
@@ -0,0 +1,4 @@
+---
+title: Added external environment link to web terminal view
+merge_request: 8303
+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/27452-update-issue-count.yml b/changelogs/unreleased/27452-update-issue-count.yml
new file mode 100644
index 00000000000..a7417eba63c
--- /dev/null
+++ b/changelogs/unreleased/27452-update-issue-count.yml
@@ -0,0 +1,4 @@
+---
+title: update issue count when closing/reopening an issue
+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/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
deleted file mode 100644
index bc990c66866..00000000000
--- a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix wrong call to ProjectCacheWorker.perform
-merge_request: 8910
-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/27608-fixes-markdown-in-activity-feed-is-light-gray.yml b/changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml
new file mode 100644
index 00000000000..8f297620e23
--- /dev/null
+++ b/changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes markdown in activity-feed is gray
+merge_request: 9179
+author:
diff --git a/changelogs/unreleased/27610-issue-number-alignment.yml b/changelogs/unreleased/27610-issue-number-alignment.yml
new file mode 100644
index 00000000000..19ab8872c62
--- /dev/null
+++ b/changelogs/unreleased/27610-issue-number-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: fixes issue number alignment problem in MR and issue list
+merge_request: 9020
+author:
diff --git a/changelogs/unreleased/27631-fix-small-height-of-activity-header-page.yml b/changelogs/unreleased/27631-fix-small-height-of-activity-header-page.yml
new file mode 100644
index 00000000000..59da28964f7
--- /dev/null
+++ b/changelogs/unreleased/27631-fix-small-height-of-activity-header-page.yml
@@ -0,0 +1,4 @@
+---
+title: "Fix small height of activity header page"
+merge_request: 8952
+author: Pavel Sorokin
diff --git a/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml b/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml
new file mode 100644
index 00000000000..6c98b46d8cb
--- /dev/null
+++ b/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dropdown width in admin project page
+merge_request: 9002
+author:
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/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/27783-fix-fe-doc-broken-link.yml b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml
new file mode 100644
index 00000000000..429110e9178
--- /dev/null
+++ b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes FE Doc broken link
+merge_request: 9120
+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/27920-both-wip-messages-showing.yml b/changelogs/unreleased/27920-both-wip-messages-showing.yml
new file mode 100644
index 00000000000..497fda8c8ba
--- /dev/null
+++ b/changelogs/unreleased/27920-both-wip-messages-showing.yml
@@ -0,0 +1,4 @@
+---
+title: Dispatch needed JS when creating a new MR in diff view
+merge_request:
+author:
diff --git a/changelogs/unreleased/27924-set-max-width-mini-pipeline-text.yml b/changelogs/unreleased/27924-set-max-width-mini-pipeline-text.yml
new file mode 100644
index 00000000000..53077eedc11
--- /dev/null
+++ b/changelogs/unreleased/27924-set-max-width-mini-pipeline-text.yml
@@ -0,0 +1,4 @@
+---
+title: Set maximum width for mini pipeline graph text so it is not truncated to early
+merge_request: 9188
+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/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/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/27994-fix-mr-widget-jump.yml b/changelogs/unreleased/27994-fix-mr-widget-jump.yml
new file mode 100644
index 00000000000..77783e54a3a
--- /dev/null
+++ b/changelogs/unreleased/27994-fix-mr-widget-jump.yml
@@ -0,0 +1,4 @@
+---
+title: Fix MR widget jump
+merge_request: 9146
+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/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/28082-deleted-branch-event-404.yml b/changelogs/unreleased/28082-deleted-branch-event-404.yml
new file mode 100644
index 00000000000..e989ca34784
--- /dev/null
+++ b/changelogs/unreleased/28082-deleted-branch-event-404.yml
@@ -0,0 +1,4 @@
+---
+title: Stop linking to deleted Branches in Activity tabs
+merge_request: 9203
+author: Jan Christophersen
diff --git a/changelogs/unreleased/28142-overlap-bugs.yml b/changelogs/unreleased/28142-overlap-bugs.yml
new file mode 100644
index 00000000000..9fdabdf204a
--- /dev/null
+++ b/changelogs/unreleased/28142-overlap-bugs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix z index issues with sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/28176_merge_widget_fix.yml b/changelogs/unreleased/28176_merge_widget_fix.yml
new file mode 100644
index 00000000000..8e4e75fc237
--- /dev/null
+++ b/changelogs/unreleased/28176_merge_widget_fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix error in MR widget after /merge slash command
+merge_request: 9259
+author:
diff --git a/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml
new file mode 100644
index 00000000000..3bcf0e06d08
--- /dev/null
+++ b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml
@@ -0,0 +1,4 @@
+---
+title: Truncate long Todo titles for non-mobile screens
+merge_request: 9311
+author:
diff --git a/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml
new file mode 100644
index 00000000000..df2478a3f28
--- /dev/null
+++ b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml
@@ -0,0 +1,4 @@
+---
+title: Pick up option from GDK to disable webpack dev server livereload
+merge_request:
+author:
diff --git a/changelogs/unreleased/28229-pipelines-loading-icon.yml b/changelogs/unreleased/28229-pipelines-loading-icon.yml
new file mode 100644
index 00000000000..d8f82f658c2
--- /dev/null
+++ b/changelogs/unreleased/28229-pipelines-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Centers loading icon vertically and horizontally in pipelines table in commit
+ view
+merge_request:
+author:
diff --git a/changelogs/unreleased/28236-browse-button-dropping.yml b/changelogs/unreleased/28236-browse-button-dropping.yml
new file mode 100644
index 00000000000..3a3d755f40c
--- /dev/null
+++ b/changelogs/unreleased/28236-browse-button-dropping.yml
@@ -0,0 +1,4 @@
+---
+title: Increase right side of file header to button stays on same line
+merge_request:
+author:
diff --git a/changelogs/unreleased/28247-timeloops-bug.yml b/changelogs/unreleased/28247-timeloops-bug.yml
new file mode 100644
index 00000000000..12ab523b7c7
--- /dev/null
+++ b/changelogs/unreleased/28247-timeloops-bug.yml
@@ -0,0 +1,4 @@
+---
+title: Only run timeago loops after rendering timeago components
+merge_request:
+author:
diff --git a/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml
new file mode 100644
index 00000000000..b13d115dab9
--- /dev/null
+++ b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fix positioning of `Scroll to top` button
+merge_request:
+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/28262-horizontal-scrolling-issue-on-long-project-names.yml b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml
new file mode 100644
index 00000000000..fa1674453de
--- /dev/null
+++ b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml
@@ -0,0 +1,4 @@
+---
+title: Wrap long Project and Group titles
+merge_request: 9301
+author:
diff --git a/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml
new file mode 100644
index 00000000000..b97e9a59b2a
--- /dev/null
+++ b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Change development tanuki favicon colors to match logo color order
+merge_request:
+author:
diff --git a/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml
new file mode 100644
index 00000000000..fed02139a5c
--- /dev/null
+++ b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml
@@ -0,0 +1,4 @@
+---
+title: Allow slashes in slash command arguments
+merge_request:
+author:
diff --git a/changelogs/unreleased/28353-little-grammar-issue.yml b/changelogs/unreleased/28353-little-grammar-issue.yml
new file mode 100644
index 00000000000..10bdb17b266
--- /dev/null
+++ b/changelogs/unreleased/28353-little-grammar-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Fix grammer issue in admin/runners
+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/28389-ux-problem-with-pipeline-coverage-placeholder.yml b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml
new file mode 100644
index 00000000000..ed357d86fe3
--- /dev/null
+++ b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: Changed coverage reg expression placeholder text to be more like a placeholder
+merge_request:
+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/28462-fix-delimiter-removes-issue-in-todo-counter.yml b/changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml
new file mode 100644
index 00000000000..80995d75c23
--- /dev/null
+++ b/changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes delimiter removes when todo marked as done
+merge_request: 9435
+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/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/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/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/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/9381-authentiq-backchannel-logout.yml b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml
new file mode 100644
index 00000000000..4dbf36cd096
--- /dev/null
+++ b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml
@@ -0,0 +1,4 @@
+---
+title: Adds remote logout functionality to the Authentiq OAuth provider
+merge_request: 9381
+author: Alexandros Keramidas
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-auto-submited-header.yml b/changelogs/unreleased/add-auto-submited-header.yml
new file mode 100644
index 00000000000..93481613b39
--- /dev/null
+++ b/changelogs/unreleased/add-auto-submited-header.yml
@@ -0,0 +1,4 @@
+---
+title: Set Auto-Submitted header to mails
+merge_request:
+author: Semyon Pupkov
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-filtered-search-to-mr.yml b/changelogs/unreleased/add-filtered-search-to-mr.yml
new file mode 100644
index 00000000000..e3577e2aec7
--- /dev/null
+++ b/changelogs/unreleased/add-filtered-search-to-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Add filtered search to MR page
+merge_request:
+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-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-yarn-documentation.yml b/changelogs/unreleased/add-yarn-documentation.yml
new file mode 100644
index 00000000000..5bcc01ac177
--- /dev/null
+++ b/changelogs/unreleased/add-yarn-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: add rake tasks to handle yarn dependencies and update documentation
+merge_request: 9316
+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/alphabetically_sort_tags_on_runner_list.yml b/changelogs/unreleased/alphabetically_sort_tags_on_runner_list.yml
new file mode 100644
index 00000000000..ffcf197a596
--- /dev/null
+++ b/changelogs/unreleased/alphabetically_sort_tags_on_runner_list.yml
@@ -0,0 +1,4 @@
+---
+title: Alphabetically sort tags on runner list
+merge_request: 8922
+author: blackst0ne
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-entities.yml b/changelogs/unreleased/api-entities.yml
new file mode 100644
index 00000000000..2003d00fd52
--- /dev/null
+++ b/changelogs/unreleased/api-entities.yml
@@ -0,0 +1,4 @@
+---
+title: "Use an entity for RepoBranch commits and enhance RepoCommit"
+merge_request: 7138
+author: Ben Boeckel
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-post-block.yml b/changelogs/unreleased/api-post-block.yml
new file mode 100644
index 00000000000..dfc61ffa9e3
--- /dev/null
+++ b/changelogs/unreleased/api-post-block.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Use POST to (un)block a user'
+merge_request: 9371
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-deploy-key-disable.yml b/changelogs/unreleased/api-remove-deploy-key-disable.yml
new file mode 100644
index 00000000000..f471ad2aa20
--- /dev/null
+++ b/changelogs/unreleased/api-remove-deploy-key-disable.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove `DELETE projects/:id/deploy_keys/:key_id/disable`'
+merge_request: 9365
+author: Robert Schilling
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-star-restful.yml b/changelogs/unreleased/api-star-restful.yml
new file mode 100644
index 00000000000..3e7de8cd822
--- /dev/null
+++ b/changelogs/unreleased/api-star-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`'
+merge_request: 9328
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-subscription-restful.yml b/changelogs/unreleased/api-subscription-restful.yml
new file mode 100644
index 00000000000..95db470e6c9
--- /dev/null
+++ b/changelogs/unreleased/api-subscription-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: - 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.'
+merge_request: 9325
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-todos-restful.yml b/changelogs/unreleased/api-todos-restful.yml
new file mode 100644
index 00000000000..dba1350a495
--- /dev/null
+++ b/changelogs/unreleased/api-todos-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Use POST requests to mark todos as done'
+merge_request: 9410
+author: Robert Schilling
diff --git a/changelogs/unreleased/artifactsdoc.yml b/changelogs/unreleased/artifactsdoc.yml
new file mode 100644
index 00000000000..4ef32d5256f
--- /dev/null
+++ b/changelogs/unreleased/artifactsdoc.yml
@@ -0,0 +1,4 @@
+---
+title: Added documentation for permalinks to most recent build artifacts.
+merge_request: 8934
+author: Christian Godenschwager
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/beautiful-karma-output.yml b/changelogs/unreleased/beautiful-karma-output.yml
new file mode 100644
index 00000000000..6ccddebab68
--- /dev/null
+++ b/changelogs/unreleased/beautiful-karma-output.yml
@@ -0,0 +1,4 @@
+---
+title: Make Karma output look nicer for CI
+merge_request: 9165
+author: winniehell
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/bypass-email-domain-validation-when-created-by-admin.yml b/changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml
new file mode 100644
index 00000000000..f335ae27fda
--- /dev/null
+++ b/changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml
@@ -0,0 +1,4 @@
+---
+title: Bypass email domain validation when a user is created by an admin.
+merge_request: 8575
+author: Reza Mohammadi @remohammadi
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/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/copy-branch-to-clipboard.yml b/changelogs/unreleased/copy-branch-to-clipboard.yml
new file mode 100644
index 00000000000..c12e324ed3c
--- /dev/null
+++ b/changelogs/unreleased/copy-branch-to-clipboard.yml
@@ -0,0 +1,4 @@
+---
+title: Added the ability to copy a branch name to the clipboard
+merge_request: 9103
+author: Glenn Sayers
diff --git a/changelogs/unreleased/cover-my-karma.yml b/changelogs/unreleased/cover-my-karma.yml
new file mode 100644
index 00000000000..4a823dc5ca4
--- /dev/null
+++ b/changelogs/unreleased/cover-my-karma.yml
@@ -0,0 +1,4 @@
+---
+title: Reintroduce coverage report for JavaScript
+merge_request: 9133
+author: winniehell
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-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-header-fixture.yml b/changelogs/unreleased/dynamic-header-fixture.yml
new file mode 100644
index 00000000000..9789a1999c8
--- /dev/null
+++ b/changelogs/unreleased/dynamic-header-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for header_spec.js
+merge_request: 9174
+author: winniehell
diff --git a/changelogs/unreleased/dynamic-project-title-fixture.yml b/changelogs/unreleased/dynamic-project-title-fixture.yml
new file mode 100644
index 00000000000..2404cbb891c
--- /dev/null
+++ b/changelogs/unreleased/dynamic-project-title-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for project_title_spec.js
+merge_request: 9175
+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-create-nested-groups-via-ui.yml b/changelogs/unreleased/dz-create-nested-groups-via-ui.yml
new file mode 100644
index 00000000000..f9529a5941a
--- /dev/null
+++ b/changelogs/unreleased/dz-create-nested-groups-via-ui.yml
@@ -0,0 +1,4 @@
+---
+title: Allow creating nested groups via UI
+merge_request: 8786
+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-api.yml b/changelogs/unreleased/dz-nested-groups-api.yml
new file mode 100644
index 00000000000..d33ff42700f
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add nested groups to the API
+merge_request: 9034
+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/dz-refactor-full-path.yml b/changelogs/unreleased/dz-refactor-full-path.yml
new file mode 100644
index 00000000000..da8568fd220
--- /dev/null
+++ b/changelogs/unreleased/dz-refactor-full-path.yml
@@ -0,0 +1,4 @@
+---
+title: Store group and project full name and full path in routes table
+merge_request: 8979
+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-paginated-environments-api-add-subview.yml b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml
new file mode 100644
index 00000000000..7e626982de6
--- /dev/null
+++ b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml
@@ -0,0 +1,4 @@
+---
+title: Adds paginationd and folders view to environments table
+merge_request:
+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-github-find-users-by-email.yml b/changelogs/unreleased/feature-github-find-users-by-email.yml
new file mode 100644
index 00000000000..1503cf2b9f7
--- /dev/null
+++ b/changelogs/unreleased/feature-github-find-users-by-email.yml
@@ -0,0 +1,4 @@
+---
+title: GitHub Importer - Find users based on GitHub email address
+merge_request: 8958
+author:
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/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-cancel-integration-settings.yml b/changelogs/unreleased/fix-cancel-integration-settings.yml
deleted file mode 100644
index 294b0aa5db9..00000000000
--- a/changelogs/unreleased/fix-cancel-integration-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed services form cancel not redirecting back the integrations settings view
-merge_request: 8843
-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-cycle-analytics-events-limit.yml b/changelogs/unreleased/fix-cycle-analytics-events-limit.yml
new file mode 100644
index 00000000000..152b37ca430
--- /dev/null
+++ b/changelogs/unreleased/fix-cycle-analytics-events-limit.yml
@@ -0,0 +1,4 @@
+---
+title: Add limit to the number of events showed in cycle analytics
+merge_request:
+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-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml
deleted file mode 100644
index 3513f5afdfb..00000000000
--- a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix filtering usernames with multiple words
-merge_request: 8851
-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-notification-settings-when-no-repository.yml b/changelogs/unreleased/fix-gb-notification-settings-when-no-repository.yml
new file mode 100644
index 00000000000..17fd1336b8e
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-notification-settings-when-no-repository.yml
@@ -0,0 +1,4 @@
+---
+title: Show notifications settings dropdown even if repository feature is disabled
+merge_request: 9180
+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-pipeline-retry-builds-started.yml b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml
new file mode 100644
index 00000000000..49e243ca6bb
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml
@@ -0,0 +1,4 @@
+---
+title: Fix CI/CD pipeline retry and take stages order into account
+merge_request: 9021
+author:
diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml
new file mode 100644
index 00000000000..d747e0e63a3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline retry and cancel buttons on pipeline details page
+merge_request: 9225
+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-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml
deleted file mode 100644
index 985a3b0b26f..00000000000
--- a/changelogs/unreleased/fix-import-user-validation-error.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove old project members when retrying an export
-merge_request:
-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-search-bar-search-param.yml b/changelogs/unreleased/fix-search-bar-search-param.yml
deleted file mode 100644
index 4df14d3bf13..00000000000
--- a/changelogs/unreleased/fix-search-bar-search-param.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix search bar search param encoding
-merge_request: 8753
-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_issue_from_milestone.yml b/changelogs/unreleased/fix_issue_from_milestone.yml
new file mode 100644
index 00000000000..02581e3ea09
--- /dev/null
+++ b/changelogs/unreleased/fix_issue_from_milestone.yml
@@ -0,0 +1,4 @@
+---
+title: fix milestone does not automatically assign when create issue from milestone
+merge_request:
+author:
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/fixes-namespace-api-documentation.yml b/changelogs/unreleased/fixes-namespace-api-documentation.yml
new file mode 100644
index 00000000000..6b578bb1602
--- /dev/null
+++ b/changelogs/unreleased/fixes-namespace-api-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Update API docs for new namespace format
+merge_request: 9073
+author: Markus Koller
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/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/gfm-autocomplete-fixes.yml b/changelogs/unreleased/gfm-autocomplete-fixes.yml
new file mode 100644
index 00000000000..737e2ad5234
--- /dev/null
+++ b/changelogs/unreleased/gfm-autocomplete-fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Fix errors in slash commands matcher, add simple test coverage
+merge_request:
+author: YarNayar
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/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/group-memebrs-owner-level.yml b/changelogs/unreleased/group-memebrs-owner-level.yml
new file mode 100644
index 00000000000..ba77f38eb6d
--- /dev/null
+++ b/changelogs/unreleased/group-memebrs-owner-level.yml
@@ -0,0 +1,4 @@
+---
+title: Added option to update to owner for group members
+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/instrument-in-karma.yml b/changelogs/unreleased/instrument-in-karma.yml
new file mode 100644
index 00000000000..cfabf2569fe
--- /dev/null
+++ b/changelogs/unreleased/instrument-in-karma.yml
@@ -0,0 +1,4 @@
+---
+title: Move babel config for instanbul to karma config
+merge_request: 9286
+author: winniehell
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-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-newproj-layout.yml b/changelogs/unreleased/issue-newproj-layout.yml
new file mode 100644
index 00000000000..d15e8b7d1e5
--- /dev/null
+++ b/changelogs/unreleased/issue-newproj-layout.yml
@@ -0,0 +1,4 @@
+---
+title: Removed duplicate "Visibility Level" label on New Project page
+merge_request:
+author: Robert Marcano
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_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_25900.yml b/changelogs/unreleased/issue_25900.yml
new file mode 100644
index 00000000000..b4b72b8a20c
--- /dev/null
+++ b/changelogs/unreleased/issue_25900.yml
@@ -0,0 +1,4 @@
+---
+title: Gather issuable metadata to avoid n+1 queries on index view
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_26701.yml b/changelogs/unreleased/issue_26701.yml
new file mode 100644
index 00000000000..6834351bf43
--- /dev/null
+++ b/changelogs/unreleased/issue_26701.yml
@@ -0,0 +1,4 @@
+---
+title: Remove JIRA closed status icon
+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/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/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/lnovy-gitlab-ce-empty-variables.yml b/changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml
new file mode 100644
index 00000000000..bd5db5ac7af
--- /dev/null
+++ b/changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml
@@ -0,0 +1,4 @@
+---
+title: 'UI: Allow a project variable to be set to an empty value'
+merge_request: 6044
+author: Lukáš Nový
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/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/move_tags_service_to_namespace.yml b/changelogs/unreleased/move_tags_service_to_namespace.yml
new file mode 100644
index 00000000000..ba76f291162
--- /dev/null
+++ b/changelogs/unreleased/move_tags_service_to_namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Move tag services to Tags namespace
+merge_request:
+author: dixpac
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/new-branch-fixture.yml b/changelogs/unreleased/new-branch-fixture.yml
new file mode 100644
index 00000000000..ce5ed816102
--- /dev/null
+++ b/changelogs/unreleased/new-branch-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for new_branch_spec.js
+merge_request: 9131
+author: winniehell
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_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/only-yield-valid-reference-matches.yml b/changelogs/unreleased/only-yield-valid-reference-matches.yml
new file mode 100644
index 00000000000..95da3cc56fd
--- /dev/null
+++ b/changelogs/unreleased/only-yield-valid-reference-matches.yml
@@ -0,0 +1,4 @@
+---
+title: Only yield valid references in ReferenceFilter.references_in
+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
new file mode 100644
index 00000000000..c2e0410cc33
--- /dev/null
+++ b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
@@ -0,0 +1,4 @@
+---
+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/paginate-all-the-things.yml b/changelogs/unreleased/paginate-all-the-things.yml
new file mode 100644
index 00000000000..52f23ba52a9
--- /dev/null
+++ b/changelogs/unreleased/paginate-all-the-things.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Paginate all endpoints that return an array'
+merge_request: 8606
+author: Robert Schilling
diff --git a/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
new file mode 100644
index 00000000000..0751047c3c0
--- /dev/null
+++ b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
@@ -0,0 +1,4 @@
+---
+title: pass in current_user in MergeRequest and MergeRequestsHelper
+merge_request: 8624
+author: Dongqing Hu
diff --git a/changelogs/unreleased/pass_coverage_value_to_commit_status_api.yml b/changelogs/unreleased/pass_coverage_value_to_commit_status_api.yml
new file mode 100644
index 00000000000..74e0c18fa67
--- /dev/null
+++ b/changelogs/unreleased/pass_coverage_value_to_commit_status_api.yml
@@ -0,0 +1,4 @@
+---
+title: Make it possible to pass coverage value to commit status API
+merge_request: 9214
+author: wendy0402
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/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/protected-branch-dropdown-titles.yml b/changelogs/unreleased/protected-branch-dropdown-titles.yml
new file mode 100644
index 00000000000..df82cc00fc9
--- /dev/null
+++ b/changelogs/unreleased/protected-branch-dropdown-titles.yml
@@ -0,0 +1,4 @@
+---
+title: Added headers to protected branch access dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/quick-submit-fixture.yml b/changelogs/unreleased/quick-submit-fixture.yml
new file mode 100644
index 00000000000..a2cf05dabec
--- /dev/null
+++ b/changelogs/unreleased/quick-submit-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for behaviors/quick_submit_spec.js
+merge_request: 9086
+author: winniehell
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/removal_of_unused_parameter.yml b/changelogs/unreleased/removal_of_unused_parameter.yml
new file mode 100644
index 00000000000..26bffafd9d9
--- /dev/null
+++ b/changelogs/unreleased/removal_of_unused_parameter.yml
@@ -0,0 +1,4 @@
+---
+title: 'removed unused parameter ''status_only: true'''
+merge_request:
+author:
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-inactive-default-email-services.yml b/changelogs/unreleased/remove-inactive-default-email-services.yml
new file mode 100644
index 00000000000..c32c1390e4e
--- /dev/null
+++ b/changelogs/unreleased/remove-inactive-default-email-services.yml
@@ -0,0 +1,4 @@
+---
+title: Remove inactive default email services
+merge_request: 8987
+author:
diff --git a/changelogs/unreleased/remove-jquery-ui-datepicker.yml b/changelogs/unreleased/remove-jquery-ui-datepicker.yml
new file mode 100644
index 00000000000..cd00690d774
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-datepicker.yml
@@ -0,0 +1,4 @@
+---
+title: Replaced jQuery UI datepicker
+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-jquery-ui-sortable.yml b/changelogs/unreleased/remove-jquery-ui-sortable.yml
new file mode 100644
index 00000000000..35f47822738
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-sortable.yml
@@ -0,0 +1,4 @@
+---
+title: Replaced jQuery UI sortable
+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/rename-retry-failed-pipeline-to-retry.yml b/changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml
new file mode 100644
index 00000000000..b813127b1e6
--- /dev/null
+++ b/changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml
@@ -0,0 +1,4 @@
+---
+title: Rename retry failed button on pipeline page to just retry
+merge_request:
+author:
diff --git a/changelogs/unreleased/rename_delete_services.yml b/changelogs/unreleased/rename_delete_services.yml
new file mode 100644
index 00000000000..686a1ef3d55
--- /dev/null
+++ b/changelogs/unreleased/rename_delete_services.yml
@@ -0,0 +1,4 @@
+---
+title: Fix inconsistent naming for services that delete things
+merge_request: 5803
+author: dixpac
diff --git a/changelogs/unreleased/rename_files_delete_service.yml b/changelogs/unreleased/rename_files_delete_service.yml
new file mode 100644
index 00000000000..4de1c5b0d63
--- /dev/null
+++ b/changelogs/unreleased/rename_files_delete_service.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Files::DeleteService to Files::DestroyService
+merge_request: 9110
+author: dixpac
diff --git a/changelogs/unreleased/replace-npm-with-yarn.yml b/changelogs/unreleased/replace-npm-with-yarn.yml
new file mode 100644
index 00000000000..5e795eb0c8d
--- /dev/null
+++ b/changelogs/unreleased/replace-npm-with-yarn.yml
@@ -0,0 +1,4 @@
+---
+title: replace npm with yarn and add yarn.lock
+merge_request: 9055
+author:
diff --git a/changelogs/unreleased/requires-input-fixture.yml b/changelogs/unreleased/requires-input-fixture.yml
new file mode 100644
index 00000000000..be674499429
--- /dev/null
+++ b/changelogs/unreleased/requires-input-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for behaviors/requires_input_spec.js
+merge_request: 9162
+author: winniehell
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/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/seed-abuse-reports.yml b/changelogs/unreleased/seed-abuse-reports.yml
new file mode 100644
index 00000000000..6fbcb81ae3f
--- /dev/null
+++ b/changelogs/unreleased/seed-abuse-reports.yml
@@ -0,0 +1,4 @@
+---
+title: Seed abuse reports for development
+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-project-id-index-project-authorizations.yml b/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml
deleted file mode 100644
index e69fcd2aa63..00000000000
--- a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add project ID index to `project_authorizations` table to optimize queries
-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/sh-delete-user-permission-check.yml b/changelogs/unreleased/sh-delete-user-permission-check.yml
new file mode 100644
index 00000000000..c0e79aae2a8
--- /dev/null
+++ b/changelogs/unreleased/sh-delete-user-permission-check.yml
@@ -0,0 +1,4 @@
+---
+title: Add user deletion permission check in `Users::DestroyService`
+merge_request:
+author:
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/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml
deleted file mode 100644
index 4867f088953..00000000000
--- a/changelogs/unreleased/snippet-spam.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Check public snippets for spam
-merge_request:
-author:
diff --git a/changelogs/unreleased/snippets-search.yml b/changelogs/unreleased/snippets-search.yml
new file mode 100644
index 00000000000..00cf34f4a48
--- /dev/null
+++ b/changelogs/unreleased/snippets-search.yml
@@ -0,0 +1,4 @@
+---
+title: Fix snippets search result spacing
+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/static-navbar.yml b/changelogs/unreleased/static-navbar.yml
new file mode 100644
index 00000000000..eaf478a48d0
--- /dev/null
+++ b/changelogs/unreleased/static-navbar.yml
@@ -0,0 +1,4 @@
+---
+title: Remove fixed positioning from top nav
+merge_request: !7547
+author:
diff --git a/changelogs/unreleased/task_list_refactor.yml b/changelogs/unreleased/task_list_refactor.yml
new file mode 100644
index 00000000000..68942dadaa8
--- /dev/null
+++ b/changelogs/unreleased/task_list_refactor.yml
@@ -0,0 +1,4 @@
+---
+title: Deduplicate markdown task lists
+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/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/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/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-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-slow-service-fetch.yml b/changelogs/unreleased/zj-slow-service-fetch.yml
deleted file mode 100644
index 8037361d2fc..00000000000
--- a/changelogs/unreleased/zj-slow-service-fetch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve performance of slash commands
-merge_request: 8876
-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 f00e58a36ca..98b2759a8a7 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")
@@ -80,39 +82,25 @@ module Gitlab
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
+ # Configure webpack
+ config.webpack.config_file = "config/webpack.config.js"
+ config.webpack.output_dir = "public/assets/webpack"
+ config.webpack.public_path = "assets/webpack"
+
+ # Webpack dev server configuration is handled in initializers/static_files.rb
+ config.webpack.dev_server.enabled = false
+
# Enable the asset pipeline
config.assets.enabled = true
- config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
- config.assets.precompile << "lib/vue_resource.js"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
- config.assets.precompile << "graphs/graphs_bundle.js"
- config.assets.precompile << "users/users_bundle.js"
- config.assets.precompile << "network/network_bundle.js"
- config.assets.precompile << "profile/profile_bundle.js"
- config.assets.precompile << "protected_branches/protected_branches_bundle.js"
- config.assets.precompile << "diff_notes/diff_notes_bundle.js"
- config.assets.precompile << "merge_request_widget/ci_bundle.js"
- config.assets.precompile << "issuable/issuable_bundle.js"
- config.assets.precompile << "boards/boards_bundle.js"
- config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
- config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
- config.assets.precompile << "boards/test_utils/simulate_drag.js"
- config.assets.precompile << "environments/environments_bundle.js"
- config.assets.precompile << "blob_edit/blob_edit_bundle.js"
- config.assets.precompile << "snippet/snippet_bundle.js"
- config.assets.precompile << "terminal/terminal_bundle.js"
- config.assets.precompile << "filtered_search/filtered_search_bundle.js"
- config.assets.precompile << "lib/utils/*.js"
- config.assets.precompile << "lib/*.js"
- config.assets.precompile << "u2f.js"
- config.assets.precompile << "vue_pipelines_index/index.js"
+ config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
@@ -130,7 +118,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
@@ -140,7 +128,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 c11296975b7..072ed8a3864 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -1,9 +1,9 @@
---
-# IGNORED GROUPS AND GEMS
- - :ignore_group
- development
- :who: Connor Shea
- :why: Development gems are not distributed with the final product and are therefore exempt.
+ :why: Development gems are not distributed with the final product and are therefore
+ exempt.
:versions: []
:when: 2016-04-17 21:27:01.054140000 Z
- - :ignore_group
@@ -18,8 +18,6 @@
:why: Bundler is MIT licensed but will sometimes fail in CI.
:versions: []
:when: 2016-05-02 06:42:08.045090000 Z
-
-# LICENSE WHITELIST
- - :whitelist
- MIT
- :who: Connor Shea
@@ -86,9 +84,6 @@
:why: https://opensource.org/licenses/BSD-2-Clause
:versions: []
:when: 2016-07-26 21:24:07.248480000 Z
-
-
-# LICENSE BLACKLIST
- - :blacklist
- GPLv2
- :who: Connor Shea
@@ -107,9 +102,6 @@
:why: The OSL license is a copyleft license
:versions: []
:when: 2016-10-28 11:02:15.540105000 Z
-
-
-# GEM LICENSES
- - :license
- raphael-rails
- MIT
@@ -201,3 +193,136 @@
:why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-05-02 05:56:50.696858000 Z
+- - :approve
+ - after
+ - :who: Matt Lee
+ :why: https://github.com/Raynos/after/blob/master/LICENCE
+ :versions: []
+ :when: 2017-01-14 20:00:32.473125000 Z
+- - :approve
+ - amdefine
+ - :who: Matt Lee
+ :why: MIT License
+ :versions: []
+ :when: 2017-01-14 20:08:31.810633000 Z
+- - :approve
+ - base64id
+ - :who: Matt Lee
+ :why: https://github.com/faeldt/base64id/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:33.174760000 Z
+- - :approve
+ - blob
+ - :who: Matt Lee
+ :why: https://github.com/webmodules/blob/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:34.564048000 Z
+- - :approve
+ - callsite
+ - :who: Matt Lee
+ :why: https://github.com/tj/callsite/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:35.976025000 Z
+- - :approve
+ - component-bind
+ - :who: Matt Lee
+ :why: https://github.com/component/bind/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:37.291219000 Z
+- - :approve
+ - component-inherit
+ - :who: Matt Lee
+ :why: https://github.com/component/inherit/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:41.804804000 Z
+- - :approve
+ - fsevents
+ - :who: Matt Lee
+ :why: https://github.com/strongloop/fsevents/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:50:20.037775000 Z
+- - :approve
+ - indexof
+ - :who: Matt Lee
+ :why: https://github.com/component/indexof/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:43.209900000 Z
+- - :approve
+ - is-integer
+ - :who: Matt Lee
+ :why: https://github.com/parshap/js-is-integer/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:44.540916000 Z
+- - :approve
+ - jsonify
+ - :who: Matt Lee
+ :why: Public Domain - no formal license on this one. probably okay as its been
+ the same for along time. would prefer to see CC0
+ :versions: []
+ :when: 2017-01-14 20:10:45.857261000 Z
+- - :approve
+ - object-component
+ - :who: Matt Lee
+ :why: https://github.com/component/object/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:47.190148000 Z
+- - :approve
+ - optimist
+ - :who: Matt Lee
+ :why: https://github.com/substack/node-optimist/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:48.563077000 Z
+- - :approve
+ - path-is-inside
+ - :who: Matt Lee
+ :why: https://github.com/domenic/path-is-inside/blob/master/LICENSE.txt
+ :versions: []
+ :when: 2017-01-14 20:10:49.910497000 Z
+- - :approve
+ - rc
+ - :who: Matt Lee
+ :why: https://github.com/dominictarr/rc/blob/master/LICENSE.MIT
+ :versions: []
+ :when: 2017-01-14 20:10:51.244695000 Z
+- - :approve
+ - ripemd160
+ - :who: Matt Lee
+ :why: https://github.com/crypto-browserify/ripemd160/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:52.560282000 Z
+- - :approve
+ - select2
+ - :who: Matt Lee
+ :why: https://github.com/select2/select2/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:53.909618000 Z
+- - :approve
+ - tweetnacl
+ - :who: Matt Lee
+ :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:57.812077000 Z
+- - :approve
+ - wordwrap
+ - :who: Mike Greiling
+ :why: https://github.com/substack/node-wordwrap/blob/0.0.3/LICENSE
+ :versions: []
+ :when: 2017-02-08 20:17:13.084968000 Z
+- - :approve
+ - spdx-expression-parse
+ - :who: Mike Greiling
+ :why: https://github.com/kemitchell/spdx-expression-parse.js/blob/v1.0.4/LICENSE
+ :versions: []
+ :when: 2017-02-08 22:33:01.806977000 Z
+- - :approve
+ - spdx-license-ids
+ - :who: Mike Greiling
+ :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 42e5f105d46..2bc39ea3f65 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -76,14 +76,6 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
- ## Default theme ID
- ## 1 - Graphite
- ## 2 - Charcoal
- ## 3 - Green
- ## 4 - Gray
- ## 5 - Violet
- ## 6 - Blue
- # default_theme: 2 # default: 2
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
@@ -97,7 +89,7 @@ production: &base
issues: true
merge_requests: true
wiki: true
- snippets: false
+ snippets: true
builds: true
container_registry: true
@@ -153,6 +145,21 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## GitLab Pages
+ pages:
+ enabled: false
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ # The domain under which the pages are served:
+ # http://group.example.com/project
+ # or project path can be a group page: group.example.com
+ 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
+
## Mattermost
## For enabling Add to Mattermost button
mattermost:
@@ -170,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 * * * *"
@@ -434,6 +441,16 @@ 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
@@ -454,7 +471,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:
@@ -476,6 +494,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:
@@ -505,6 +525,16 @@ production: &base
# Git timeout to read a commit, in seconds
timeout: 10
+ ## Webpack settings
+ # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running
+ # on a given port instead of serving directly from /assets/webpack. This is only indended for use
+ # in development.
+ webpack:
+ # dev_server:
+ # enabled: true
+ # host: localhost
+ # port: 3808
+
#
# 5. Extra customization
# ==========================
@@ -555,7 +585,8 @@ test:
path: tmp/tests/gitlab-satellites/
repositories:
storages:
- default: tmp/tests/repositories/
+ default:
+ path: tmp/tests/repositories/
backup:
path: tmp/tests/backups
gitlab_shell:
@@ -569,7 +600,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
@@ -586,4 +617,4 @@ test:
admin_group: ''
staging:
- <<: *base \ No newline at end of file
+ <<: *base
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 4f33aad8693..d049ae9476f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -6,7 +6,7 @@ class Settings < Settingslogic
class << self
def gitlab_on_standard_port?
- gitlab.port.to_i == (gitlab.https ? 443 : 80)
+ on_standard_port?(gitlab)
end
def host_without_www(url)
@@ -14,12 +14,15 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if gitlab_on_standard_port?
- 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,
@@ -27,6 +30,10 @@ class Settings < Settingslogic
].join('')
end
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
@@ -42,11 +49,11 @@ class Settings < Settingslogic
end
def build_base_gitlab_url
- base_gitlab_url.join('')
+ base_url(gitlab).join('')
end
def build_gitlab_url
- (base_gitlab_url + [gitlab.relative_url_root]).join('')
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
@@ -74,15 +81,21 @@ class Settings < Settingslogic
private
- def base_gitlab_url
- custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
- [ gitlab.protocol,
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+
+ [
+ config.protocol,
"://",
- gitlab.host,
+ config.host,
custom_port
]
end
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
@@ -152,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({})
@@ -172,10 +186,9 @@ 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['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
@@ -208,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)
@@ -255,6 +268,20 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
#
+# Pages
+#
+Settings['pages'] ||= Settingslogic.new({})
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root)
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+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?
+
+#
# Git LFS
#
Settings['lfs'] ||= Settingslogic.new({})
@@ -281,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'
@@ -339,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
@@ -349,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
@@ -372,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
@@ -410,6 +443,15 @@ Settings['gitaly'] ||= Settingslogic.new({})
Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
#
+# Webpack settings
+#
+Settings['webpack'] ||= Settingslogic.new({})
+Settings.webpack['dev_server'] ||= Settingslogic.new({})
+Settings.webpack.dev_server['enabled'] ||= false
+Settings.webpack.dev_server['host'] ||= 'localhost'
+Settings.webpack.dev_server['port'] ||= 3808
+
+#
# Testing settings
#
if Rails.env.test?
diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb
deleted file mode 100644
index d252e403102..00000000000
--- a/config/initializers/4_ci_app.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module GitlabCi
- VERSION = Gitlab::VERSION
- REVISION = Gitlab::REVISION
-
- def self.config
- Settings
- end
-end
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..3e1657b8382
--- /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 '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/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/additional_headers_interceptor.rb b/config/initializers/additional_headers_interceptor.rb
new file mode 100644
index 00000000000..b9159e7c06c
--- /dev/null
+++ b/config/initializers/additional_headers_interceptor.rb
@@ -0,0 +1 @@
+ActionMailer::Base.register_interceptor(AdditionalEmailHeadersInterceptor)
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index a8afc36fc78..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
@@ -240,6 +240,17 @@ Devise.setup do |config|
true
end
end
+ if provider['name'] == 'authentiq'
+ provider['args'][:remote_sign_out_handler] = lambda do |request|
+ authentiq_session = request.params['sid']
+ if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session)
+ Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session)
+ true
+ else
+ false
+ end
+ end
+ end
if provider['name'] == 'shibboleth'
provider['args'][:fail_with_empty_uid] = true
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/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb
index 835f3ec5574..9a569be7674 100644
--- a/config/initializers/mysql_ignore_postgresql_options.rb
+++ b/config/initializers/mysql_ignore_postgresql_options.rb
@@ -31,7 +31,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
end
def add_index_options(table_name, column_name, options = {})
- if options[:using] and options[:using] == :gin
+ if options[:using] && options[:using] == :gin
options = options.dup
options.delete(:using)
end
diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb
new file mode 100644
index 00000000000..e8a77b146fa
--- /dev/null
+++ b/config/initializers/plantuml_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Plantuml
diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb
index 22e77a32c61..f7172fce9bc 100644
--- a/config/initializers/rack_lineprof.rb
+++ b/config/initializers/rack_lineprof.rb
@@ -1,7 +1,7 @@
# The default colors of rack-lineprof can be very hard to look at in terminals
# with darker backgrounds. This patch tweaks the colors a bit so the output is
# actually readable.
-if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF']
+if Rails.env.development? && RUBY_ENGINE == 'ruby' && ENV['ENABLE_LINEPROF']
Rails.application.config.middleware.use(Rack::Lineprof)
module Rack
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/request_profiler.rb b/config/initializers/request_profiler.rb
index a9aa802681a..fb5a7b8372e 100644
--- a/config/initializers/request_profiler.rb
+++ b/config/initializers/request_profiler.rb
@@ -1,5 +1,3 @@
-require 'gitlab/request_profiler/middleware'
-
Rails.application.configure do |config|
config.middleware.use(Gitlab::RequestProfiler::Middleware)
end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index f462e654b2c..ac353d14499 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -1,14 +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
+
+ module Git
+ def branch
+ ENV['CI_BUILD_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
+
+ 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 fa318384405..2b018c68703 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,11 +42,9 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
- # Database pool should be at least `sidekiq_concurrency` + 2
- # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
- config['pool'] = Sidekiq.options[:concurrency] + 2
+ config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d6dbf8b9fbf..74aba6c5d06 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -12,4 +12,35 @@ if app.config.serve_static_files
app.paths["public"].first,
app.config.static_cache_control
)
+
+ # If webpack-dev-server is configured, proxy webpack's public directory
+ # instead of looking for static assets
+ dev_server = Gitlab.config.webpack.dev_server
+
+ if dev_server.enabled
+ settings = {
+ enabled: true,
+ host: dev_server.host,
+ port: dev_server.port,
+ manifest_host: dev_server.host,
+ manifest_port: dev_server.port,
+ }
+
+ if Rails.env.development?
+ settings.merge!(
+ host: Gitlab.config.gitlab.host,
+ port: Gitlab.config.gitlab.port,
+ https: Gitlab.config.gitlab.https,
+ )
+ app.config.middleware.insert_before(
+ Gitlab::Middleware::Static,
+ Gitlab::Middleware::WebpackProxy,
+ proxy_path: app.config.webpack.public_path,
+ proxy_host: dev_server.host,
+ proxy_port: dev_server.port,
+ )
+ end
+
+ app.config.webpack.dev_server.merge!(settings)
+ end
end
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
new file mode 100644
index 00000000000..a23e62f5022
--- /dev/null
+++ b/config/karma.config.js
@@ -0,0 +1,51 @@
+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.module && webpackConfig.module.rules) {
+ var babelConfig = webpackConfig.module.rules.find(function (rule) {
+ return rule.loader === 'babel-loader';
+ });
+
+ babelConfig.options = babelConfig.options || {};
+ babelConfig.options.plugins = babelConfig.options.plugins || [];
+ babelConfig.options.plugins.push('istanbul');
+}
+
+// remove problematic plugins
+if (webpackConfig.plugins) {
+ webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
+ return !(
+ plugin instanceof webpack.optimize.CommonsChunkPlugin ||
+ plugin instanceof webpack.DefinePlugin
+ );
+ });
+}
+
+// Karma configuration
+module.exports = function(config) {
+ var progressReporter = process.env.CI ? 'mocha' : 'progress';
+ config.set({
+ basePath: ROOT_PATH,
+ browsers: ['PhantomJS'],
+ frameworks: ['jasmine'],
+ files: [
+ { pattern: 'spec/javascripts/test_bundle.js', watched: false },
+ { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
+ ],
+ preprocessors: {
+ 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+ },
+ reporters: [progressReporter, 'coverage-istanbul'],
+ coverageIstanbulReporter: {
+ reports: ['html', 'text-summary'],
+ dir: 'coverage-javascript/',
+ subdir: '.',
+ fixWebpackSourcePaths: true
+ },
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ });
+};
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/mail_room.yml b/config/mail_room.yml
index 774c5350a45..88d93d4bc6b 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -1,9 +1,6 @@
-# If you change this file in a Merge Request, please also create
-# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
:mailboxes:
<%
- require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
+ require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
config = Gitlab::MailRoom.config
if Gitlab::MailRoom.enabled?
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 8e99239f350..486ce3c5c87 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 fb20c63bc63..adc3ad207cc 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -14,6 +14,9 @@ resource :dashboard, controller: 'dashboard', only: [] do
collection do
delete :destroy_all
end
+ member do
+ patch :restore
+ end
end
resources :projects, only: [:index] do
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 60a1175fe80..73f69d76995 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -25,5 +25,6 @@ scope(path: 'groups/*id',
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
+ get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical
end
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 efe2fbc521d..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'
@@ -39,6 +38,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :pages, only: [:show, :destroy] do
+ resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
+ end
+
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
@@ -54,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
@@ -96,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
@@ -131,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
@@ -150,8 +159,13 @@ 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
+
+ collection do
+ get :folder, path: 'folders/:id'
+ end
end
resource :cycle_analytics, only: [:show]
@@ -257,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
@@ -265,9 +279,11 @@ 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: [:update]
+ resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
@@ -311,7 +327,9 @@ constraints(ProjectUrlConstrainer.new) do
end
namespace :settings 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/sidekiq.rb b/config/routes/sidekiq.rb
index d3e6bc4c292..0fa23f2b3d0 100644
--- a/config/routes/sidekiq.rb
+++ b/config/routes/sidekiq.rb
@@ -1,4 +1,4 @@
-constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
+constraint = lambda { |request| request.env['warden'].authenticate? && request.env['warden'].user.admin? }
constraints constraint do
mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
end
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 022b0e80917..9d2066a6490 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -21,7 +21,7 @@
- [post_receive, 5]
- [merge, 5]
- [update_merge_requests, 3]
- - [process_commit, 2]
+ - [process_commit, 3]
- [new_note, 2]
- [build, 2]
- [pipeline, 2]
@@ -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]
@@ -50,3 +51,5 @@
- [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
+ - [pages, 1]
+ - [system_hook_push, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
new file mode 100644
index 00000000000..8e2b11a4145
--- /dev/null
+++ b/config/webpack.config.js
@@ -0,0 +1,187 @@
+'use strict';
+
+var fs = require('fs');
+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: {
+ 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',
+ cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
+ commit_pipelines: './commit/pipelines/pipelines_bundle.js',
+ diff_notes: './diff_notes/diff_notes_bundle.js',
+ environments: './environments/environments_bundle.js',
+ 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',
+ 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',
+ vue_pipelines: './vue_pipelines_index/index.js',
+ },
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ publicPath: '/assets/webpack/',
+ filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
+ },
+
+ devtool: 'inline-source-map',
+
+ module: {
+ rules: [
+ {
+ test: /\.(js|es6)$/,
+ exclude: /(node_modules|vendor\/assets)/,
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ ["es2015", {"modules": false}],
+ 'stage-2'
+ ]
+ }
+ },
+ {
+ test: /\.svg$/,
+ use: 'raw-loader'
+ }
+ ]
+ },
+
+ plugins: [
+ // manifest filename must match config.webpack.manifest_filename
+ // webpack-rails only needs assetsByChunkName to function properly
+ new StatsPlugin('manifest.json', {
+ chunkModules: false,
+ source: false,
+ chunks: false,
+ 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'],
+ }),
+
+ // create cacheable common library bundles
+ new webpack.optimize.CommonsChunkPlugin({
+ names: ['main', 'common', 'runtime'],
+ }),
+ ],
+
+ resolve: {
+ extensions: ['.js', '.es6', '.js.es6'],
+ alias: {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ '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$': 'vue/dist/vue.common.js',
+ }
+ }
+}
+
+if (IS_PRODUCTION) {
+ config.devtool = 'source-map';
+ config.plugins.push(
+ new webpack.NoEmitOnErrorsPlugin(),
+ new webpack.LoaderOptionsPlugin({
+ minimize: true,
+ debug: false
+ }),
+ new webpack.optimize.UglifyJsPlugin({
+ sourceMap: true
+ }),
+ new webpack.DefinePlugin({
+ 'process.env': { NODE_ENV: JSON.stringify('production') }
+ }),
+ new CompressionPlugin({
+ asset: '[path].gz[query]',
+ })
+ );
+}
+
+if (IS_DEV_SERVER) {
+ config.devServer = {
+ port: DEV_SERVER_PORT,
+ headers: { 'Access-Control-Allow-Origin': '*' },
+ stats: 'errors-only',
+ inline: DEV_SERVER_LIVERELOAD
+ };
+ 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/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index c04afe97277..c304e0706dc 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -26,7 +26,7 @@ Gitlab::Seeder.quiet do
end
end
- project = Project.find_with_namespace('gitlab-org/gitlab-test')
+ project = Project.find_by_full_path('gitlab-org/gitlab-test')
params = {
source_branch: 'feature',
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..aea0a72b633 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", options, 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/18_abuse_reports.rb b/db/fixtures/development/18_abuse_reports.rb
new file mode 100644
index 00000000000..8618d10387a
--- /dev/null
+++ b/db/fixtures/development/18_abuse_reports.rb
@@ -0,0 +1,5 @@
+require 'factory_girl_rails'
+
+(AbuseReport.default_per_page + 3).times do
+ FactoryGirl.create(:abuse_report)
+end
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/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
new file mode 100644
index 00000000000..f3a663f805b
--- /dev/null
+++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
@@ -0,0 +1,14 @@
+class AddPagesSizeToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false
+ end
+
+ def down
+ remove_column(:application_settings, :max_pages_size)
+ end
+end
diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb
new file mode 100644
index 00000000000..0e8507c7e9a
--- /dev/null
+++ b/db/migrate/20160210105555_create_pages_domain.rb
@@ -0,0 +1,16 @@
+class CreatePagesDomain < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :pages_domains do |t|
+ t.integer :project_id
+ t.text :certificate
+ t.text :encrypted_key
+ t.string :encrypted_key_iv
+ t.string :encrypted_key_salt
+ t.string :domain
+ end
+
+ add_index :pages_domains, :domain, unique: true
+ end
+end
diff --git a/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
index 15ad8e8bcbb..ac50035eba4 100644
--- a/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
+++ b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
@@ -1,9 +1,15 @@
class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false
end
+
+ def down
+ remove_column :protected_branches, :developers_can_merge
+ end
end
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/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
index 296f1dfac7b..20a77000ba8 100644
--- a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
@@ -14,7 +14,11 @@ class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
end
+
+ def down
+ remove_column :spam_logs, :submitted_as_ham
+ 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 05e21af0584..828b6afddb1 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
+ 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_index :labels, :group_id if index_exists? :labels, :group_id
+ remove_foreign_key :labels, :namespaces, column: :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 f49df6802a7..ad3eb4a26f9 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_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
- add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade
+ end
+
+ def down
+ remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id
+ remove_foreign_key :merge_request_metrics, :ci_commits, column: :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 97534679b59..d5c343dc527 100644
--- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -5,7 +5,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
def up
add_column :subscriptions, :project_id, :integer
- add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade
+ add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
end
def down
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/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/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
index e9fcef1cd45..d7ef1aa83d9 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], unique: true 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/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
new file mode 100644
index 00000000000..f90637e1e35
--- /dev/null
+++ b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 00000000000..69bfa2d3fc4
--- /dev/null
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -0,0 +1,54 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddForeignKeysToTimelogs < 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
+ change_table :timelogs do |t|
+ t.column :issue_id, :integer
+ t.column :merge_request_id, :integer
+ end
+
+ add_concurrent_index :timelogs, :issue_id
+ add_concurrent_index :timelogs, :merge_request_id
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
+ EOF
+ else
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
+ end
+
+ Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
+ Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
+ end
+
+ def down
+ 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_columns :timelogs, :issue_id, :merge_request_id
+ end
+end
diff --git a/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb
new file mode 100644
index 00000000000..334f53f9145
--- /dev/null
+++ b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTerminalMaxSessionTimeToApplicationSettings < 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_with_default :application_settings, :terminal_max_session_time, :integer, default: 0, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :terminal_max_session_time
+ end
+end
diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
new file mode 100644
index 00000000000..0ee4229d1f8
--- /dev/null
+++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
@@ -0,0 +1,17 @@
+class RemoveBacklogListsFromBoards < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ DELETE FROM lists WHERE list_type = 0;
+ SQL
+ end
+
+ def down
+ execute <<-SQL
+ INSERT INTO lists (board_id, list_type, created_at, updated_at)
+ SELECT boards.id, 0, NOW(), NOW()
+ FROM boards;
+ SQL
+ 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/20170204172458_add_name_to_route.rb b/db/migrate/20170204172458_add_name_to_route.rb
new file mode 100644
index 00000000000..38ed1ad9039
--- /dev/null
+++ b/db/migrate/20170204172458_add_name_to_route.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNameToRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :routes, :name, :string
+ 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
new file mode 100644
index 00000000000..31ef458c44f
--- /dev/null
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -0,0 +1,15 @@
+class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ 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/20170206071414_add_recaptcha_verified_to_spam_logs.rb b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
new file mode 100644
index 00000000000..44372334d21
--- /dev/null
+++ b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
@@ -0,0 +1,15 @@
+class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:spam_logs, :recaptcha_verified)
+ 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
new file mode 100644
index 00000000000..70fb0ef12f9
--- /dev/null
+++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
@@ -0,0 +1,17 @@
+class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ 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
new file mode 100644
index 00000000000..07d4f8af27f
--- /dev/null
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -0,0 +1,15 @@
+class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ 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
new file mode 100644
index 00000000000..2d8329b7862
--- /dev/null
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.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 AddIndexToUserAgentDetail < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ 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..65adc90c2c1
--- /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..906711b9f3f
--- /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, [: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/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
new file mode 100644
index 00000000000..3eece0e2eb5
--- /dev/null
+++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
@@ -0,0 +1,11 @@
+class AddOwnerIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade
+ 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/20170206040400_remove_inactive_default_email_services.rb b/db/post_migrate/20170206040400_remove_inactive_default_email_services.rb
new file mode 100644
index 00000000000..a8e63e8bc7d
--- /dev/null
+++ b/db/post_migrate/20170206040400_remove_inactive_default_email_services.rb
@@ -0,0 +1,41 @@
+class RemoveInactiveDefaultEmailServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::Database.with_connection_pool(2) do |pool|
+ threads = []
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ connection.execute <<-SQL.strip_heredoc
+ DELETE FROM services
+ WHERE type = 'BuildsEmailService'
+ AND active IS FALSE
+ AND properties = '{"notify_only_broken_builds":true}';
+ SQL
+ end
+ end
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ connection.execute <<-SQL.strip_heredoc
+ DELETE FROM services
+ WHERE type = 'PipelinesEmailService'
+ AND active IS FALSE
+ AND properties = '{"notify_only_broken_pipelines":true}';
+ SQL
+ end
+ end
+
+ threads.each(&:join)
+ end
+ end
+
+ def down
+ # Nothing can be done to restore the records
+ 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
new file mode 100644
index 00000000000..89aa753646c
--- /dev/null
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # 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
+ remove_columns :timelogs, :trackable_id, :trackable_type
+ end
+end
diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
new file mode 100644
index 00000000000..f397ef919cc
--- /dev/null
+++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # 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
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
+ EOF
+ end
+ end
+
+ def down
+ # noop
+ 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
new file mode 100644
index 00000000000..603efc43782
--- /dev/null
+++ b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
@@ -0,0 +1,13 @@
+class DisableInvalidServiceTemplates < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class Service < ActiveRecord::Base
+ self.inheritance_column = nil
+ end
+
+ def up
+ Service.where(template: true, active: true).each do |template|
+ template.update(active: false) unless template.valid?
+ end
+ end
+end
diff --git a/db/post_migrate/20170214111112_delete_deprecated_gitlab_ci_service.rb b/db/post_migrate/20170214111112_delete_deprecated_gitlab_ci_service.rb
new file mode 100644
index 00000000000..09a827d22b0
--- /dev/null
+++ b/db/post_migrate/20170214111112_delete_deprecated_gitlab_ci_service.rb
@@ -0,0 +1,15 @@
+class DeleteDeprecatedGitlabCiService < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ disable_statement_timeout
+
+ execute("DELETE FROM services WHERE type = 'GitlabCiService';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20170215200045_remove_theme_id_from_users.rb b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb
new file mode 100644
index 00000000000..c51646fbe52
--- /dev/null
+++ b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb
@@ -0,0 +1,9 @@
+class RemoveThemeIdFromUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :users, :theme_id, :integer
+ 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..9020e0d054c
--- /dev/null
+++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
@@ -0,0 +1,19 @@
+class MigrateLegacyManualActions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ 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
+ 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/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
new file mode 100644
index 00000000000..9dfe77bedb7
--- /dev/null
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -0,0 +1,101 @@
+require 'thread'
+
+class RenameMoreReservedProjectNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ DOWNTIME = false
+
+ THREAD_COUNT = 8
+
+ KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
+
+ def up
+ queues = Array.new(THREAD_COUNT) { Queue.new }
+ start = false
+
+ threads = Array.new(THREAD_COUNT) do |index|
+ Thread.new do
+ queue = queues[index]
+
+ # Wait until we have input to process.
+ until start; end
+
+ rename_projects(queue.pop) until queue.empty?
+ end
+ end
+
+ enum = queues.each
+
+ reserved_projects.each_slice(100) do |slice|
+ begin
+ queue = enum.next
+ rescue StopIteration
+ enum.rewind
+ retry
+ end
+
+ queue << slice
+ end
+
+ start = true
+
+ threads.each(&:join)
+ 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 c73c311ccb2..ca88198079f 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: 20170130204620) do
+ActiveRecord::Schema.define(version: 20170313133418) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
+ t.integer "max_pages_size", default: 100, null: false
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
@@ -98,17 +99,22 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
+ t.integer "terminal_max_session_time", default: 0, 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|
@@ -169,6 +175,16 @@ ActiveRecord::Schema.define(version: 20170130204620) 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 "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_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
@@ -249,8 +265,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.integer "lock_version"
end
+ add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree
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
@@ -365,6 +381,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.integer "commit_id"
end
+ add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
+
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
t.integer "project_id"
@@ -372,6 +390,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "gl_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
@@ -510,6 +530,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
+ t.integer "relative_position"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -521,6 +542,7 @@ ActiveRecord::Schema.define(version: 20170130204620) 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"}
@@ -576,6 +598,9 @@ ActiveRecord::Schema.define(version: 20170130204620) do
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+ add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
+ add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -681,7 +706,7 @@ ActiveRecord::Schema.define(version: 20170130204620) 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"
@@ -748,8 +773,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.boolean "lfs_enabled"
t.text "description_html"
+ t.boolean "lfs_enabled"
t.integer "parent_id"
end
@@ -855,15 +880,32 @@ ActiveRecord::Schema.define(version: 20170130204620) 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"
+ t.text "encrypted_key"
+ t.string "encrypted_key_iv"
+ t.string "encrypted_key_salt"
+ t.string "domain"
+ end
+
+ add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
+
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
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
@@ -953,7 +995,7 @@ ActiveRecord::Schema.define(version: 20170130204620) 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
@@ -1023,6 +1065,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.string "path", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "name"
end
add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
@@ -1100,6 +1143,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
+ t.boolean "recaptcha_verified", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -1136,14 +1180,15 @@ ActiveRecord::Schema.define(version: 20170130204620) do
create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false
- t.integer "trackable_id"
- t.string "trackable_type"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "issue_id"
+ t.integer "merge_request_id"
end
- add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
+ add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
+ add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
create_table "todos", force: :cascade do |t|
@@ -1187,6 +1232,20 @@ ActiveRecord::Schema.define(version: 20170130204620) 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
@@ -1197,6 +1256,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "updated_at", null: false
end
+ add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -1217,7 +1278,6 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
@@ -1255,9 +1315,11 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "organization"
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
@@ -1308,6 +1370,8 @@ ActiveRecord::Schema.define(version: 20170130204620) 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
@@ -1318,6 +1382,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
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
@@ -1325,6 +1390,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
+ add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
+ add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index 909740211a6..57d85d770e7 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -12,16 +12,18 @@
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [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](project_services/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.
-- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
+- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
@@ -49,12 +51,14 @@
- [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)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
+- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics.
+- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
index 3f39539da95..fb1a16b0f96 100644
--- a/doc/administration/auth/authentiq.md
+++ b/doc/administration/auth/authentiq.md
@@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers.
-6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1.
+6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1.
7. Save the configuration file.
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/build_artifacts.md b/doc/administration/build_artifacts.md
index cca422892ec..623a5321f32 100644
--- a/doc/administration/build_artifacts.md
+++ b/doc/administration/build_artifacts.md
@@ -1,96 +1 @@
-# Build artifacts administration
-
->**Notes:**
->- Introduced in GitLab 8.2 and GitLab Runner 0.7.0.
->- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
- changed to `ZIP`.
->- This is the administration documentation. For the user guide see
- [user/project/builds/artifacts.md](../user/project/builds/artifacts.md).
-
-Artifacts is a list of files and directories which are attached to a build
-after it completes successfully. This feature is enabled by default in all
-GitLab installations. Keep reading if you want to know how to disable it.
-
-## Disabling build artifacts
-
-To disable artifacts site-wide, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
-
- ```ruby
- gitlab_rails['artifacts_enabled'] = false
- ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
- ```yaml
- artifacts:
- enabled: false
- ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Storing build artifacts
-
-After a successful build, GitLab Runner uploads an archive containing the build
-artifacts to GitLab.
-
-To change the location where the artifacts are stored, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-_The artifacts are stored by default in
-`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
- `/etc/gitlab/gitlab.rb` and add the following line:
-
- ```ruby
- gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
- ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-_The artifacts are stored by default in
-`/home/git/gitlab/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
- `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
- ```yaml
- artifacts:
- enabled: true
- path: /mnt/storage/artifacts
- ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Set the maximum file size of the artifacts
-
-Provided the artifacts are enabled, you can change the maximum file size of the
-artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size).
-
-[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
-[restart gitlab]: restart_gitlab.md "How to restart GitLab"
-
-## Storage statistics
-
-You can see the total storage used for build artifacts on groups and projects
-in the administration area, as well as through the [groups](../api/groups.md)
-and [projects APIs](../api/projects.md).
+This document was moved to [job_artifacts](job_artifacts.md).
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index a6300e18dc0..f707039827b 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -466,12 +466,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])**
@@ -492,3 +588,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/custom_hooks.md b/doc/administration/custom_hooks.md
index 80e5d80aa41..4d35b20d0c3 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -3,7 +3,7 @@
>
**Note:** Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not
+Please explore [webhooks] as an option if you do not
have filesystem access. For a user configurable Git hook interface, please see
[GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).
@@ -80,5 +80,6 @@ STDERR takes precedence over STDOUT.
![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
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 1824829903c..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.
@@ -66,4 +66,4 @@ Read more on high-availability configuration:
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
-[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html
+[gitlab-pages]: ../pages/index.md
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 5602d70f1ef..3893d837006 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -47,13 +47,13 @@ When using default Omnibus configuration you will need to share 5 data locations
between all GitLab cluster nodes. No other locations should be shared. The
following are the 5 locations you need to mount:
-| Location | Description |
-| -------- | ----------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
Other GitLab directories should not be shared between nodes. They contain
node-specific files and GitLab code that does not need to be shared. To ship
@@ -73,10 +73,10 @@ as subdirectories. Mount `/gitlab-data` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
+git_data_dirs({"default" => "/gitlab-data/git-data"})
user['home'] = '/gitlab-data/home'
-git_data_dir '/gitlab-data/git-data'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_ci['builds_directory'] = '/gitlab-data/builds'
```
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index e5cf592e0a6..6515b1a264a 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -3,8 +3,8 @@
> [Introduced][ce-7810] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in
-GitLab we are able to create simple diagrams in AsciiDoc documents created in
-snippets, wikis, and repos.
+GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
+created in snippets, wikis, and repos.
## PlantUML Server
@@ -54,7 +54,7 @@ that, login with an Admin account and do following:
## Creating Diagrams
With PlantUML integration enabled and configured, we can start adding diagrams to
-our AsciiDoc snippets, wikis and repos using blocks:
+our AsciiDoc snippets, wikis and repos using delimited blocks:
```
[plantuml, format="png", id="myDiagram", width="200px"]
@@ -64,7 +64,14 @@ Alice -> Bob : Go Away
--
```
-The above block will be converted to an HTML img tag with source pointing to the
+And in Markdown using fenced code blocks:
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
+
+The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
render a nice diagram instead of the block:
@@ -77,7 +84,7 @@ Inside the block you can add any of the supported diagrams by PlantUML such as
and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML
diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block.
-Some parameters can be added to the block definition:
+Some parameters can be added to the AsciiDoc block definition:
- *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
all browsers so use with care. The default is `png`.
@@ -85,3 +92,4 @@ Some parameters can be added to the block definition:
- *width*: Width attribute added to the img tag.
- *height*: Height attribute added to the img tag.
+Markdown does not support any parameters and will always use PNG format.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index a1d1bb03b50..3b5ee86b68b 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -1,13 +1,12 @@
# Web terminals
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
-in GitLab 8.15. Only project masters and owners can access web terminals.
+> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can
+ access web terminals.
-With the introduction of the [Kubernetes](../../project_services/kubernetes.md)
-project service, GitLab gained the ability to store and use credentials for a
-Kubernetes cluster. One of the things it uses these credentials for is providing
-access to [web terminals](../../ci/environments.html#web-terminals)
-for environments.
+With the introduction of the [Kubernetes project service][kubservice], GitLab
+gained the ability to store and use credentials for a Kubernetes cluster. One
+of the things it uses these credentials for is providing access to
+[web terminals](../../ci/environments.html#web-terminals) for environments.
## How it works
@@ -71,3 +70,16 @@ by the above guides.
When these headers are not passed through, Workhorse will return a
`400 Bad Request` response to users attempting to use a web terminal. In turn,
they will receive a `Connection failed` message.
+
+## Limiting WebSocket connection time
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8413)
+in GitLab 8.17.
+
+Terminal sessions use long-lived connections; by default, these may last
+forever. You can configure a maximum session time in the Admin area of your
+GitLab instance if you find this undesirable from a scalability or security
+point of view.
+
+[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
+[kubservice]: ../../user/project/integrations/kubernetes.md
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
new file mode 100644
index 00000000000..7b0610ae414
--- /dev/null
+++ b/doc/administration/job_artifacts.md
@@ -0,0 +1,114 @@
+# Jobs artifacts administration
+
+>**Notes:**
+>- Introduced in GitLab 8.2 and GitLab Runner 0.7.0.
+>- Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+ changed to `ZIP`.
+>- Starting with GitLab 8.17, builds are renamed to jobs.
+>- This is the administration documentation. For the user guide see
+ [pipelines/job_artifacts](../user/project/pipelines/job_artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a job
+after it completes successfully. This feature is enabled by default in all
+GitLab installations. Keep reading if you want to know how to disable it.
+
+## Disabling job artifacts
+
+To disable artifacts site-wide, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_enabled'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: false
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Storing job artifacts
+
+After a successful job, GitLab Runner uploads an archive containing the job
+artifacts to GitLab.
+
+To change the location where the artifacts are stored, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+_The artifacts are stored by default in
+`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+_The artifacts are stored by default in
+`/home/git/gitlab/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: true
+ path: /mnt/storage/artifacts
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Set the maximum file size of the artifacts
+
+Provided the artifacts are enabled, you can change the maximum file size of the
+artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size).
+
+## Storage statistics
+
+You can see the total storage used for job artifacts on groups and projects
+in the administration area, as well as through the [groups](../api/groups.md)
+and [projects APIs](../api/projects.md).
+
+## Implementation details
+
+When GitLab receives an artifacts archive, an archive metadata file is also
+generated. This metadata file describes all the entries that are located in the
+artifacts archive itself. The metadata file is in a binary format, with
+additional GZIP compression.
+
+GitLab does not extract the artifacts archive in order to save space, memory
+and disk I/O. It instead inspects the metadata file which contains all the
+relevant information. This is especially important when there is a lot of
+artifacts, or an archive is a very large file.
+
+When clicking on a specific file, [GitLab Workhorse] extracts it
+from the archive and the download begins. This implementation saves space,
+memory and disk I/O.
+
+[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
+[restart gitlab]: restart_gitlab.md "How to restart GitLab"
+[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 8b106e89cc2..17c2b4b70d3 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -15,7 +15,7 @@ documents in order to understand and properly configure GitLab Performance Monit
>**Note:**
Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
metrics. It will eventually replace InfluxDB when their metrics collection is
-on par. Read more in the [Prometheus documentation](prometheus.md).
+on par. Read more in the [Prometheus documentation](../prometheus/index.md).
## Introduction to GitLab Performance Monitoring
diff --git a/doc/administration/monitoring/performance/prometheus.md b/doc/administration/monitoring/performance/prometheus.md
index 51c63325064..d73ef5d1789 100644
--- a/doc/administration/monitoring/performance/prometheus.md
+++ b/doc/administration/monitoring/performance/prometheus.md
@@ -1,102 +1 @@
-# GitLab Prometheus
-
->**Notes:**
-- Prometheus and the node exporter are bundled in the Omnibus GitLab package
- since GitLab 8.16. 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.
-
-[Prometheus] is a powerful time-series monitoring service, providing a flexible
-platform for monitoring GitLab and other software products.
-GitLab provides out of the box monitoring with Prometheus, providing easy
-access to high quality time-series monitoring of GitLab services.
-
-## Overview
-
-Prometheus works by periodically connecting to data sources and collecting their
-performance metrics. To view and work with the monitoring data, you can either
-connect directly to Prometheus or utilize a 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.
-
-To enable Prometheus:
-
-1. Edit `/etc/gitlab/gitlab.rb`
-1. Find and uncomment the following line, making sure it's set to `true`:
-
- ```ruby
- prometheus['enable'] = true
- ```
-
-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
-TCP port `9090` under localhost. If the [node exporter](#node-exporter) service
-has been enabled, it will automatically be set up as a monitoring target for
-Prometheus.
-
-## Viewing Performance Metrics
-
-After you have [enabled Prometheus](#configuring-prometheus), you can visit
-`<your_domain_name>:9090` for the dashboard that Prometheus offers by default.
-
-The performance data collected by Prometheus can be viewed directly in the
-Prometheus console or through a compatible dashboard tool.
-The Prometheus interface provides a [flexible query language][prom-query] to work
-with the collected data where you can visualize their output.
-For a more fully featured dashboard, Grafana can be used and has
-[official support for Prometheus][prom-grafana].
-
-## Prometheus exporters
-
-There are a number of libraries and servers which help in exporting existing
-metrics from third-party systems as Prometheus metrics. This is useful for cases
-where it is not feasible to instrument a given system with Prometheus metrics
-directly (for example, HAProxy or Linux system stats). You can read more in the
-[Prometheus exporters and integrations documentation][prom-exporters].
-
-While you can use any exporter you like with your GitLab installation, the
-following ones documented here are bundled in the Omnibus GitLab packages
-making it easy to configure and use.
-
-### Node exporter
-
->**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
-
-The [node exporter] allows you to measure various machine resources such as
-memory, disk and CPU utilization.
-
-To enable the node exporter:
-
-1. [Enable Prometheus](#configuring-prometheus)
-1. Edit `/etc/gitlab/gitlab.rb`
-1. Find and uncomment the following line, making sure it's set to `true`:
-
- ```ruby
- node_exporter['enable'] = true
- ```
-
-1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
- take effect
-
-Prometheus it will now automatically begin collecting performance data from
-the node exporter. You can visit `<your_domain_name>:9100/metrics` for a real
-time representation of the metrics that are collected. Refresh the page and
-you will see the data change.
-
-[grafana]: https://grafana.net
-[node exporter]: https://github.com/prometheus/node_exporter
-[prometheus]: https://prometheus.io
-[prom-query]: https://prometheus.io/docs/querying/basics
-[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
-[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
-[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
-[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
+This document was moved to [monitoring/prometheus](../prometheus/index.md).
diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
new file mode 100644
index 00000000000..edb9c911aac
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
@@ -0,0 +1,30 @@
+# GitLab monitor exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1132]. For installations from source
+you'll have to install and configure it yourself.
+
+The [GitLab monitor exporter] allows you to measure various GitLab metrics.
+
+To enable the GitLab monitor exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ gitlab_monitor['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the GitLab monitor exporter exposed under `localhost:9168`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1132]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1132
+[GitLab monitor exporter]: https://gitlab.com/gitlab-org/gitlab-monitor
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
new file mode 100644
index 00000000000..3a394c561db
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -0,0 +1,147 @@
+# GitLab Prometheus
+
+>**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.
+- Prometheus and its exporters do not authenticate users, and will be available
+ to anyone who can access them.
+
+[Prometheus] is a powerful time-series monitoring service, providing a flexible
+platform for monitoring GitLab and other software products.
+GitLab provides out of the box monitoring with Prometheus, providing easy
+access to high quality time-series monitoring of GitLab services.
+
+## Overview
+
+Prometheus works by periodically connecting to data sources and collecting their
+performance metrics via the [various exporters](#prometheus-exporters). To view
+and work with the monitoring data, you can either
+[connect directly to Prometheus](#viewing-performance-metrics) or utilize a
+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.
+
+To enable Prometheus:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ prometheus['enable'] = true
+ ```
+
+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:**
+The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
+it's not recommended to change the default address and port Prometheus listens
+on as this might affect or conflict with other services running on the GitLab
+server. Proceed at your own risk.
+
+To change the address/port that Prometheus listens on:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line:
+
+ ```ruby
+ prometheus['listen_address'] = 'localhost:9090'
+ ```
+
+ Replace `localhost:9090` with the address/port you want Prometheus to
+ listen on.
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+## Viewing performance metrics
+
+After you have [enabled Prometheus](#configuring-prometheus), 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.
+
+The performance data collected by Prometheus can be viewed directly in the
+Prometheus console or through a compatible dashboard tool.
+The Prometheus interface provides a [flexible query language][prom-query] to work
+with the collected data where you can visualize their output.
+For a more fully featured dashboard, Grafana can be used and has
+[official support for Prometheus][prom-grafana].
+
+Sample Prometheus queries:
+
+- **% Memory used:** `(1 - ((node_memory_MemFree + node_memory_Cached) / node_memory_MemTotal)) * 100`
+- **% CPU load:** `1 - rate(node_cpu{mode="idle"}[5m])`
+- **Data transmitted:** `irate(node_network_transmit_bytes[5m])`
+- **Data received:** `irate(node_network_receive_bytes[5m])`
+
+## Prometheus exporters
+
+There are a number of libraries and servers which help in exporting existing
+metrics from third-party systems as Prometheus metrics. This is useful for cases
+where it is not feasible to instrument a given system with Prometheus metrics
+directly (for example, HAProxy or Linux system stats). You can read more in the
+[Prometheus exporters and integrations upstream documentation][prom-exporters].
+
+While you can use any exporter you like with your GitLab installation, the
+following ones documented here are bundled in the Omnibus GitLab packages
+making it easy to configure and use.
+
+### Node exporter
+
+The node exporter allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+[➔ Read more about the node exporter.](node_exporter.md)
+
+### Redis exporter
+
+The Redis exporter allows you to measure various Redis metrics.
+
+[➔ Read more about the Redis exporter.](redis_exporter.md)
+
+### Postgres exporter
+
+The Postgres exporter allows you to measure various PostgreSQL metrics.
+
+[➔ Read more about the Postgres exporter.](postgres_exporter.md)
+
+### GitLab monitor exporter
+
+The GitLab monitor exporter allows you to measure various GitLab metrics.
+
+[➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md)
+
+[grafana]: https://grafana.net
+[hsts]: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
+[multi-user-prometheus]: https://gitlab.com/gitlab-org/multi-user-prometheus
+[nginx-custom-config]: https://docs.gitlab.com/omnibus/settings/nginx.html#inserting-custom-nginx-settings-into-the-gitlab-server-block
+[prometheus]: https://prometheus.io
+[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
+[prom-query]: https://prometheus.io/docs/querying/basics
+[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
diff --git a/doc/administration/monitoring/prometheus/node_exporter.md b/doc/administration/monitoring/prometheus/node_exporter.md
new file mode 100644
index 00000000000..aef7758a88f
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/node_exporter.md
@@ -0,0 +1,30 @@
+# Node exporter
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+The [node exporter] allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+To enable the node exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ node_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the node exporter exposed under `localhost:9100`.
+
+[← Back to the main Prometheus page](index.md)
+
+[node exporter]: https://github.com/prometheus/node_exporter
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/postgres_exporter.md b/doc/administration/monitoring/prometheus/postgres_exporter.md
new file mode 100644
index 00000000000..8e2d3162f88
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/postgres_exporter.md
@@ -0,0 +1,30 @@
+# Postgres exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1131]. For installations from source
+you'll have to install and configure it yourself.
+
+The [postgres exporter] allows you to measure various PostgreSQL metrics.
+
+To enable the postgres exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ postgres_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the postgres exporter exposed under `localhost:9187`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1131]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1131
+[postgres exporter]: https://github.com/wrouesnel/postgres_exporter
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/redis_exporter.md b/doc/administration/monitoring/prometheus/redis_exporter.md
new file mode 100644
index 00000000000..d54d409dbb6
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/redis_exporter.md
@@ -0,0 +1,33 @@
+# Redis exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1118]. For installations from source
+you'll have to install and configure it yourself.
+
+The [Redis exporter] allows you to measure various [Redis] metrics. For more
+information on what's exported [read the upstream documentation][redis-exp].
+
+To enable the Redis exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ redis_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the Redis exporter exposed under `localhost:9121`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1118]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1118
+[redis]: https://redis.io
+[redis exporter]: https://github.com/oliver006/redis_exporter
+[redis-exp]: https://github.com/oliver006/redis_exporter/blob/master/README.md#whats-exported
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
new file mode 100644
index 00000000000..62b0468da79
--- /dev/null
+++ b/doc/administration/pages/index.md
@@ -0,0 +1,278 @@
+# GitLab Pages administration
+
+> **Notes:**
+- [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 guide is for Omnibus GitLab installations. If you have installed
+ GitLab from source, follow the [Pages source installation document](source.md).
+- To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
+
+---
+
+This document describes how to set up the _latest_ GitLab Pages feature. Make
+sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
+version as it may include new features and changes needed to be made in your
+configuration.
+
+## 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.
+
+---
+
+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.
+
+## Prerequisites
+
+Before proceeding with the Pages configuration, you will need to:
+
+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)
+ so that your users don't have to bring their own.
+1. (Only for custom domains) Have a **secondary IP**.
+
+### DNS configuration
+
+GitLab Pages expect to run on their own virtual host. In your DNS server/provider
+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
+```
+
+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.
+
+> **Note:**
+You should not use the GitLab domain to serve user pages. For more information
+see the [security section](#security).
+
+[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
+
+## Configuration
+
+Depending on your needs, you can set up GitLab Pages in 4 different ways.
+The following options are listed from the easiest setup to the most
+advanced one. The absolute minimum requirement is to set up the wildcard DNS
+since that is needed in all configurations.
+
+### Wildcard domains
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+>
+>---
+>
+URL scheme: `http://page.example.io`
+
+This is the minimum setup that you can use Pages with. It is the base for all
+other setups as described below. Nginx will proxy all requests to the daemon.
+The Pages daemon doesn't listen to the outside world.
+
+1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url 'http://example.io'
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+Watch the [video tutorial][video-admin] for this configuration.
+
+### Wildcard domains with TLS support
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+>
+>---
+>
+URL scheme: `https://page.example.io`
+
+Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
+outside world.
+
+1. Place the certificate and key inside `/etc/gitlab/ssl`
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
+
+ ```ruby
+ pages_external_url 'https://example.io'
+
+ pages_nginx['redirect_http_to_https'] = true
+ pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
+ pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
+ ```
+
+ where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key,
+ respectively.
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Advanced configuration
+
+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.
+
+### Custom domains
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Secondary IP
+>
+---
+>
+URL scheme: `http://page.example.io` and `http://domain.com`
+
+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.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ 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'
+ ```
+
+ 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. [Reconfigure GitLab][reconfigure]
+
+### Custom domains with TLS support
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+- Secondary IP
+>
+---
+>
+URL scheme: `https://page.example.io` and `https://domain.com`
+
+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.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "https://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ 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'
+ ```
+
+ 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. [Reconfigure GitLab][reconfigure]
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_path'] = "/mnt/storage/pages"
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Set maximum pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+## Changelog
+
+GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features
+where added, like custom CNAME and TLS support, and many more are likely to
+come. Below is a brief changelog. If no changes were introduced or a version is
+missing from the changelog, assume that the documentation is the same as the
+latest previous version.
+
+---
+
+**GitLab 8.17 ([documentation][8-17-docs])**
+
+- GitLab Pages were ported to Community Edition in GitLab 8.17.
+- Documentation was refactored to be more modular and easy to follow.
+
+**GitLab 8.5 ([documentation][8-5-docs])**
+
+- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the
+ recommended way to set up GitLab Pages.
+- The [NGINX configs][] have changed to reflect this change. So make sure to
+ update them.
+- Custom CNAME and TLS certificates support.
+- Documentation was moved to one place.
+
+**GitLab 8.3 ([documentation][8-3-docs])**
+
+- GitLab Pages feature was introduced.
+
+[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md
+[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md
+[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md
+[backup]: ../../raketasks/backup_restore.md
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[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
+[video-admin]: https://youtu.be/dD8c7WNcc6s
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
new file mode 100644
index 00000000000..f6f50e2c571
--- /dev/null
+++ b/doc/administration/pages/source.md
@@ -0,0 +1,396 @@
+# GitLab Pages administration for source installations
+
+This is the documentation for configuring a GitLab Pages when you have installed
+GitLab from source and not using the Omnibus packages.
+
+You are encouraged to read the [Omnibus documentation](index.md) as it provides
+some invaluable information to the configuration of GitLab Pages. Please proceed
+to read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading GitLab
+Pages to the latest supported version.
+
+## Overview
+
+[Read the Omnibus overview section.](index.md#overview)
+
+## 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
+
+GitLab Pages expect to run on their own virtual host. In your DNS server/provider
+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
+```
+
+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.
+
+> **Note:**
+You should not use the GitLab domain to serve user pages. For more information
+see the [security section](#security).
+
+[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
+
+## Configuration
+
+Depending on your needs, you can set up GitLab Pages in 4 different ways.
+The following options are listed from the easiest setup to the most
+advanced one. The absolute minimum requirement is to set up the wildcard DNS
+since that is needed in all configurations.
+
+### Wildcard domains
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+>
+>---
+>
+URL scheme: `http://page.example.io`
+
+This is the minimum setup that you can use Pages with. It is the base for all
+other setups as described below. Nginx will proxy all requests to the daemon.
+The Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ 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 make
+ ```
+
+1. Go to the GitLab installation directory:
+
+ ```bash
+ cd /home/git/gitlab
+ ```
+
+1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
+ the `host` to the FQDN under which GitLab Pages will be served:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+ ```
+
+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
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Wildcard domains with TLS support
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+>
+>---
+>
+URL scheme: `https://page.example.io`
+
+Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
+outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ 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 make
+ ```
+
+1. In `gitlab.yml`, set the port to `443` and https to `true`:
+
+ ```bash
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+ ```
+
+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
+ ```
+
+ 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
+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.
+
+### Custom domains
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Secondary IP
+>
+---
+>
+URL scheme: `http://page.example.io` and `http://domain.com`
+
+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.
+
+1. Install the Pages daemon:
+
+ ```
+ 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 make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` to the secondary IP on which the pages daemon will listen
+ for connections:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+
+ external_http: 1.1.1.2:80
+ ```
+
+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` and `-listen-http` must match the `host` and `external_http`
+ settings that you set above respectively:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
+ ```
+
+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
+ ```
+
+ 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.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Custom domains with TLS support
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+- Secondary IP
+>
+---
+>
+URL scheme: `https://page.example.io` and `https://domain.com`
+
+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.
+
+1. Install the Pages daemon:
+
+ ```
+ 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 make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` and `external_https` to the secondary IP on which the pages
+ daemon will listen for connections:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+
+ external_http: 1.1.1.2:80
+ external_https: 1.1.1.2:443
+ ```
+
+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`, `-listen-http` and `-listen-https` must match the `host`,
+ `external_http` and `external_https` settings that you set above respectively.
+ 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 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
+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
+ ```
+
+ 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.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_path'] = "/mnt/storage/pages"
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## NGINX caveats
+
+>**Note:**
+The following information applies only for installations from source.
+
+Be extra careful when setting up the domain name in the NGINX config. You must
+not remove the backslashes.
+
+If your GitLab pages domain is `example.io`, replace:
+
+```bash
+server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+```
+
+with:
+
+```
+server_name ~^.*\.example\.io$;
+```
+
+If you are using a subdomain, make sure to escape all dots (`.`) except from
+the first one with a backslash (\). For example `pages.example.io` would be:
+
+```
+server_name ~^.*\.pages\.example\.io$;
+```
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/home/git/gitlab/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `gitlab.yml` under the `pages` section:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ path: /mnt/storage/pages
+ ```
+
+1. [Restart GitLab][restart]
+
+## Set maximum Pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+[backup]: ../../raketasks/backup_restore.md
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[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
+[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/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index 22f10489a6c..3b8c716eff5 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -315,7 +315,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
## Done!
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure 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 20f28e8d30e..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)
@@ -49,6 +49,7 @@ following locations:
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
+- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
### Internal CI API
@@ -88,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).
@@ -126,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
@@ -158,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. |
@@ -205,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,
@@ -216,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
@@ -232,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
@@ -246,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:
@@ -257,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 ffcfea41453..83705106160 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -13,7 +13,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:
@@ -34,6 +34,8 @@ Example response:
"committer_email": "john@example.com",
"committer_name": "John Smith",
"id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c",
+ "short_id": "7b5c3cc",
+ "title": "add projects API",
"message": "add projects API",
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
@@ -58,7 +60,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:
@@ -78,6 +80,8 @@ Example response:
"committer_email": "john@example.com",
"committer_name": "John Smith",
"id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c",
+ "short_id": "7b5c3cc",
+ "title": "add projects API",
"message": "add projects API",
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
@@ -97,7 +101,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 |
@@ -119,6 +123,8 @@ Example response:
"committer_email": "john@example.com",
"committer_name": "John Smith",
"id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c",
+ "short_id": "7b5c3cc",
+ "title": "add projects API",
"message": "add projects API",
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
@@ -143,7 +149,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 |
@@ -163,6 +169,8 @@ Example response:
"committer_email": "john@example.com",
"committer_name": "John Smith",
"id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c",
+ "short_id": "7b5c3cc",
+ "title": "add projects API",
"message": "add projects API",
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
@@ -185,11 +193,11 @@ POST /projects/:id/repository/branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `branch_name` | string | yes | The name of the branch |
+| `branch` | string | yes | The name of the branch |
| `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_name=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:
@@ -204,6 +212,8 @@ Example response:
"committer_email": "john@example.com",
"committer_name": "John Smith",
"id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c",
+ "short_id": "7b5c3cc",
+ "title": "add projects API",
"message": "add projects API",
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
@@ -231,15 +241,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
@@ -256,5 +258,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 53ce381c8ae..24c402346b1 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -12,11 +12,11 @@ GET /projects/:id/repository/commits
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
-| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
-| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
+| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
+| `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:
@@ -29,11 +29,15 @@ Example response:
"title": "Replace sanitize with escape once",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dzaporozhets@sphereconsultinginc.com",
+ "authored_date": "2012-09-20T11:50:22+03:00",
"committer_name": "Administrator",
"committer_email": "admin@example.com",
+ "committed_date": "2012-09-20T11:50:22+03:00",
"created_at": "2012-09-20T11:50:22+03:00",
"message": "Replace sanitize with escape once",
- "allow_failure": false
+ "parent_ids": [
+ "6104942438c14ec7bd21c6cd5bd995272b3faff6"
+ ]
},
{
"id": "6104942438c14ec7bd21c6cd5bd995272b3faff6",
@@ -45,7 +49,9 @@ Example response:
"committer_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2012-09-20T09:06:12+03:00",
"message": "Sanitize for network graph",
- "allow_failure": false
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ]
}
]
```
@@ -63,7 +69,7 @@ POST /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
-| `branch_name` | string | yes | The name of a branch |
+| `branch` | string | yes | The name of a branch |
| `commit_message` | string | yes | Commit message |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address |
@@ -81,7 +87,7 @@ POST /projects/:id/repository/commits
```bash
PAYLOAD=$(cat << 'JSON'
{
- "branch_name": "master",
+ "branch": "master",
"commit_message": "some commit message",
"actions": [
{
@@ -108,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:
@@ -153,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:
@@ -202,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:
@@ -214,10 +220,16 @@ Example response:
"title": "Feature added",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
+ "authored_date": "2016-12-12T20:10:39.000+01:00",
"created_at": "2016-12-12T20:10:39.000+01:00",
"committer_name": "Administrator",
"committer_email": "admin@example.com",
- "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+ "committed_date": "2016-12-12T20:10:39.000+01:00",
+ "title": "Feature added",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
+ "parent_ids": [
+ "a738f717824ff53aebad8b090c1b79a14f2bd9e8"
+ ]
}
```
@@ -237,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:
@@ -273,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:
@@ -326,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:
@@ -371,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:
@@ -444,9 +456,10 @@ POST /projects/:id/statuses/:sha
| `name` or `context` | string | no | The label to differentiate this status from the status of other systems. Default value is `default`
| `target_url` | string | no | The target URL to associate with this status
| `description` | string | no | The short description of the status
+| `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:
@@ -464,6 +477,7 @@ Example response:
"name" : "default",
"sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8",
"status" : "success",
+ "coverage": 100.0,
"description" : null,
"id" : 93,
"target_url" : null,
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 284d5f88c55..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:
@@ -137,7 +137,7 @@ Example response:
## Delete deploy key
-Delete a deploy key from a project
+Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system.
```
DELETE /projects/:id/deploy_keys/:key_id
@@ -149,22 +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
-{
- "updated_at" : "2015-08-29T12:50:57.259Z",
- "key" : "ssh-rsa AAAA...",
- "public" : false,
- "title" : "My deploy key",
- "user_id" : null,
- "created_at" : "2015-08-29T12:50:57.259Z",
- "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43",
- "id" : 13
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/13"
```
## Enable a deploy key
@@ -172,31 +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
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `key_id` | integer | yes | The ID of the deploy key |
-
-Example response:
-
-```json
-{
- "key" : "ssh-rsa AAAA...",
- "id" : 12,
- "title" : "My deploy key",
- "created_at" : "2015-08-29T12:44:31.550Z"
-}
-```
-
-## Disable a deploy key
-
-Disable a deploy key for a project. Returns the disabled key.
-
-```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable
+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 3b38e3e1bee..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,33 +27,20 @@ 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",
"request_access_enabled": false,
"full_name": "Foobar Group",
- "full_path": "foo-bar"
+ "full_path": "foo-bar",
+ "parent_id": null
}
]
```
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.
@@ -72,6 +60,8 @@ Parameters:
| `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 |
| `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 |
+| `starred` | boolean | no | Limit by projects starred by the current user |
Example response:
@@ -82,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",
@@ -95,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",
@@ -105,21 +94,13 @@ Example response:
"id": 5,
"name": "Experimental",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
+ "kind": "group"
},
"avatar_url": null,
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
}
@@ -141,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:
@@ -152,21 +133,21 @@ 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,
- "full_name": "Foobar Group",
- "full_path": "foo-bar",
+ "full_name": "Twitter",
+ "full_path": "twitter",
+ "parent_id": null,
"projects": [
{
"id": 7,
"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",
@@ -177,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",
@@ -188,21 +169,13 @@ Example response:
"id": 4,
"name": "Twitter",
"path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
},
@@ -211,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",
@@ -224,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",
@@ -235,21 +207,13 @@ Example response:
"id": 4,
"name": "Twitter",
"path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 8,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
}
@@ -260,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",
@@ -273,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",
@@ -284,21 +247,13 @@ Example response:
"id": 5,
"name": "H5bp",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:26.621Z",
- "updated_at": "2016-06-17T07:47:26.621Z",
- "description": "Id consequatur rem vel qui doloremque saepe.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 4,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [
{
"group_id": 4,
@@ -329,9 +284,10 @@ 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.
## Transfer project to group
@@ -360,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"
```
@@ -377,12 +333,13 @@ 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,
"full_name": "Foobar Group",
"full_path": "foo-bar",
+ "parent_id": null,
"projects": [
{
"id": 9,
@@ -391,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",
@@ -402,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",
@@ -412,21 +369,13 @@ Example response:
"id": 5,
"name": "Experimental",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
+ "kind": "group"
},
"avatar_url": null,
"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 b276d1ad918..cb437ffb174 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -23,20 +23,24 @@ GET /issues?state=closed
GET /issues?labels=foo
GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
+GET /projects/:id/issues?labels_name=No+Label
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 with any of the labels will be returned |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels_name` | string | no | Return all issues with the mentioned label. `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 +84,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",
@@ -100,8 +103,10 @@ GET /groups/:id/issues?state=closed
GET /groups/:id/issues?labels=foo
GET /groups/:id/issues?labels=foo,bar
GET /groups/:id/issues?labels=foo,bar&state=opened
+GET /projects/:id/issues?labels_name=No+Label
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 |
@@ -109,13 +114,15 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened
| `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_name` | string | no | Return all issues with the mentioned label. `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 +166,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",
@@ -179,24 +185,26 @@ GET /projects/:id/issues?state=closed
GET /projects/:id/issues?labels=foo
GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
+GET /projects/:id/issues?labels_name=No+Label
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
-GET /projects/:id/issues?iid=42
+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 with any of the labels will be returned |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels_name` | string | no | Return all issues with the mentioned label. `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:
@@ -240,7 +248,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",
@@ -254,16 +261,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:
@@ -322,21 +329,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:
@@ -378,25 +386,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:
@@ -437,16 +445,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
@@ -459,17 +467,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:
@@ -515,16 +523,16 @@ If the user is already subscribed to the issue, the status code `304`
is returned.
```
-POST /projects/:id/issues/:issue_id/subscription
+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/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe
```
Example response:
@@ -570,53 +578,16 @@ from it. If the user is not subscribed to the issue, the
status code `304` is returned.
```
-DELETE /projects/:id/issues/:issue_id/subscription
+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 DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
-```
-
-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
@@ -626,16 +597,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:
@@ -717,17 +688,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:
@@ -746,16 +717,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:
@@ -774,17 +745,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:
@@ -803,16 +774,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:
@@ -829,16 +800,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/keys.md b/doc/api/keys.md
index b68f08a007d..3b55c2baf56 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -33,7 +33,6 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
- "theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 863b28c23b7..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:
@@ -188,12 +172,12 @@ Example response:
## Subscribe to a label
-Subscribes the authenticated user to a label to receive notifications.
+Subscribes the authenticated user to a label to receive notifications.
If the user is already subscribed to the label, the status code `304`
is returned.
```
-POST /projects/:id/labels/:label_id/subscription
+POST /projects/:id/labels/:label_id/subscribe
```
| Attribute | Type | Required | Description |
@@ -202,7 +186,7 @@ POST /projects/:id/labels/:label_id/subscription
| `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/subscription
+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/subscription
+POST /projects/:id/labels/:label_id/unsubscribe
```
| Attribute | Type | Required | Description |
@@ -237,21 +221,5 @@ DELETE /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
-```
-
-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 7b005591545..2e0545da1c4 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all
-GET /projects/:id/merge_requests?iid=42
-GET /projects/:id/merge_requests?iid[]=42&iid[]=43
+GET /projects/:id/merge_requests?iids[]=42&iids[]=43
```
Parameters:
@@ -66,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,
@@ -84,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
{
@@ -135,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",
@@ -152,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
@@ -189,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
{
@@ -240,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",
@@ -271,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
{
@@ -327,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",
@@ -344,22 +342,23 @@ 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 |
-| `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 |
+| `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.
```json
{
@@ -403,7 +402,7 @@ PUT /projects/:id/merge_requests/:merge_request_id
"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 @@ PUT /projects/:id/merge_requests/:merge_request_id
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/subscription
+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/subscription
+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/subscription
+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 DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+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 12497acff98..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:
@@ -103,3 +105,16 @@ Parameters:
- `id` (required) - The ID of a project
- `milestone_id` (required) - The ID of a project milestone
+
+## Get all merge requests assigned to a single milestone
+
+Gets all merge requests assigned to a single project milestone.
+
+```
+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
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 88cd407d792..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:
@@ -35,6 +35,12 @@ Example response:
"id": 2,
"path": "group1",
"kind": "group"
+ },
+ {
+ "id": 3,
+ "path": "bar",
+ "kind": "group",
+ "full_path": "foo/bar",
}
]
```
@@ -54,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:
@@ -64,7 +70,8 @@ Example response:
{
"id": 4,
"path": "twitter",
- "kind": "group"
+ "kind": "group",
+ "full_path": "twitter",
}
]
```
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 5ef5e3f5744..46fe64d382e 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -57,7 +57,7 @@ Once you have the authorization code you can request an `access_token` using the
```
parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
-RestClient.post 'http://localhost:3000/oauth/token', parameters
+RestClient.post 'http://gitlab.example.com/oauth/token', parameters
# The response will be
{
@@ -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://localhost:3000/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://localhost:3000/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 82351ae688f..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 failed 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 c6685f54a9d..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
@@ -51,7 +51,6 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
- "expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/example/example/snippets/1"
@@ -72,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
@@ -89,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 122075bbd11..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,8 +34,11 @@ 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
[
@@ -43,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",
@@ -64,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,
@@ -72,13 +74,11 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
@@ -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,
@@ -125,13 +124,11 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 4,
"name": "Brightbox",
- "owner_id": 1,
"path": "brightbox",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "brightbox"
},
"permissions": {
"project_access": {
@@ -149,205 +146,15 @@ 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
}
]
```
-Get a list of projects which the authenticated user can see.
-
-```
-GET /projects/visible
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `archived` | boolean | no | Limit by archived status |
-| `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 |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
-
-```json
-[
- {
- "id": 4,
- "description": null,
- "default_branch": "master",
- "public": false,
- "visibility_level": 0,
- "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",
- "tag_list": [
- "example",
- "disapora client"
- ],
- "owner": {
- "id": 3,
- "name": "Diaspora",
- "created_at": "2013-09-30T13:46:02Z"
- },
- "name": "Diaspora Client",
- "name_with_namespace": "Diaspora / Diaspora Client",
- "path": "diaspora-client",
- "path_with_namespace": "diaspora/diaspora-client",
- "issues_enabled": true,
- "open_issues_count": 1,
- "merge_requests_enabled": true,
- "builds_enabled": true,
- "wiki_enabled": true,
- "snippets_enabled": false,
- "container_registry_enabled": false,
- "created_at": "2013-09-30T13:46:02Z",
- "last_activity_at": "2013-09-30T13:46:02Z",
- "creator_id": 3,
- "namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
- "id": 3,
- "name": "Diaspora",
- "owner_id": 1,
- "path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
- },
- "archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
- "shared_runners_enabled": true,
- "forks_count": 0,
- "star_count": 0,
- "runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true,
- "shared_with_groups": []
- },
- {
- "id": 6,
- "description": null,
- "default_branch": "master",
- "public": false,
- "visibility_level": 0,
- "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",
- "tag_list": [
- "example",
- "puppet"
- ],
- "owner": {
- "id": 4,
- "name": "Brightbox",
- "created_at": "2013-09-30T13:46:02Z"
- },
- "name": "Puppet",
- "name_with_namespace": "Brightbox / Puppet",
- "path": "puppet",
- "path_with_namespace": "brightbox/puppet",
- "issues_enabled": true,
- "open_issues_count": 1,
- "merge_requests_enabled": true,
- "builds_enabled": true,
- "wiki_enabled": true,
- "snippets_enabled": false,
- "container_registry_enabled": false,
- "created_at": "2013-09-30T13:46:02Z",
- "last_activity_at": "2013-09-30T13:46:02Z",
- "creator_id": 3,
- "namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
- "id": 4,
- "name": "Brightbox",
- "owner_id": 1,
- "path": "brightbox",
- "updated_at": "2013-09-30T13:46:02Z"
- },
- "permissions": {
- "project_access": {
- "access_level": 10,
- "notification_level": 3
- },
- "group_access": {
- "access_level": 50,
- "notification_level": 3
- }
- },
- "archived": false,
- "avatar_url": null,
- "shared_runners_enabled": true,
- "forks_count": 0,
- "star_count": 0,
- "runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true,
- "shared_with_groups": []
- }
-]
-```
-
-### List owned projects
-
-Get a list of projects which are owned by the authenticated user.
-
-```
-GET /projects/owned
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `archived` | boolean | no | Limit by archived status |
-| `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 |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
-| `statistics` | boolean | no | Include project statistics |
-
-### List starred projects
-
-Get a list of projects which are starred by the authenticated user.
-
-```
-GET /projects/starred
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `archived` | boolean | no | Limit by archived status |
-| `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 |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
-
-### List ALL projects
-
-Get a list of all GitLab projects (admin only).
-
-```
-GET /projects/all
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `archived` | boolean | no | Limit by archived status |
-| `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 |
-| `statistics` | boolean | no | Include project statistics |
-
### Get single project
Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user.
@@ -369,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",
@@ -390,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,
@@ -398,13 +204,11 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"permissions": {
"project_access": {
@@ -422,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,
@@ -435,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
}
@@ -601,8 +405,6 @@ Parameters:
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -631,22 +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 |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `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 |
@@ -671,16 +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 |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `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 |
@@ -704,16 +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 |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `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 |
@@ -723,7 +522,7 @@ Parameters:
Forks a project into the user namespace of the authenticated user or the one provided.
```
-POST /projects/fork/:id
+POST /projects/:id/fork
```
Parameters:
@@ -748,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:
@@ -758,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",
@@ -774,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,
@@ -782,22 +580,20 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"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
}
@@ -808,7 +604,7 @@ Example response:
Unstars a given project. Returns status code `304` if the project is not starred.
```
-DELETE /projects/:id/star
+POST /projects/:id/unstar
```
| Attribute | Type | Required | Description |
@@ -816,7 +612,7 @@ DELETE /projects/:id/star
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request DELETE --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/unstar"
```
Example response:
@@ -826,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",
@@ -842,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,
@@ -850,22 +645,20 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"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
}
@@ -885,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:
@@ -895,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",
@@ -916,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,
@@ -924,13 +716,11 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"permissions": {
"project_access": {
@@ -948,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
}
@@ -970,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:
@@ -980,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",
@@ -1001,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,
@@ -1009,13 +798,11 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group",
+ "full_path": "diaspora"
},
"permissions": {
"project_access": {
@@ -1033,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
}
@@ -1121,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
@@ -1168,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,
@@ -1195,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 |
@@ -1221,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 |
@@ -1400,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 8a6baed5987..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_name=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:
@@ -54,14 +69,14 @@ Example response:
```json
{
"file_name": "app/project.rb",
- "branch_name": "master"
+ "branch": "master"
}
```
Parameters:
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `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
- `author_name` (optional) - Specify the commit author's name
@@ -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_name=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:
@@ -83,14 +98,14 @@ Example response:
```json
{
"file_name": "app/project.rb",
- "branch_name": "master"
+ "branch": "master"
}
```
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `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
- `author_name` (optional) - Specify the commit author's name
@@ -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 PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=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:
@@ -121,14 +136,14 @@ Example response:
```json
{
"file_name": "app/project.rb",
- "branch_name": "master"
+ "branch": "master"
}
```
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `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
- `commit_message` (required) - Commit message
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 1466b8189b0..8e7afe41b0c 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -148,7 +148,7 @@ Get emails for GitLab CI builds.
Set Build-Emails service for a project.
```
-PUT /projects/:id/services/builds-email
+PUT /projects/:id/services/jobs-email
```
Parameters:
@@ -157,23 +157,23 @@ 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 builds |
+| `notify_only_broken_jobs` | boolean | no | Notify only broken jobs |
-### Delete Build-Emails service
+### Delete Job-Emails service
Delete Build-Emails service for a project.
```
-DELETE /projects/:id/services/builds-email
+DELETE /projects/:id/services/jobs-email
```
-### Get Build-Emails service settings
+### Get Job-Emails service settings
Get Build-Emails service settings for a project.
```
-GET /projects/:id/services/builds-email
+GET /projects/:id/services/jobs-email
```
## Campfire
@@ -580,7 +580,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_jobs` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
@@ -808,5 +808,40 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
-[jira-doc]: ../project_services/jira.md
+[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 f776424023e..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:
@@ -41,7 +41,6 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
- "theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
diff --git a/doc/api/settings.md b/doc/api/settings.md
index f86c7cc2f94..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,
@@ -46,7 +47,8 @@ Example response:
"koding_enabled": false,
"koding_url": null,
"plantuml_enabled": false,
- "plantuml_url": null
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
@@ -58,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. |
@@ -84,9 +87,10 @@ PUT /application/settings
| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
+| `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:
@@ -94,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,
@@ -106,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" : [],
@@ -118,6 +123,7 @@ Example response:
"koding_enabled": false,
"koding_url": null,
"plantuml_enabled": false,
- "plantuml_url": null
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
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 a5e81801024..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
@@ -184,7 +184,7 @@ Marks a single pending todo given by its ID for the current user as done. The
todo marked as done is returned in the response.
```
-DELETE /todos/:id
+POST /todos/:id/mark_as_done
```
Parameters:
@@ -194,7 +194,7 @@ Parameters:
| `id` | integer | yes | The ID of a todo |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+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
@@ -277,20 +277,15 @@ Example Response:
## Mark all todos as done
-Marks all pending todos for the current user as done. It returns the number of marked todos.
+Marks all pending todos for the current user as done. It returns the HTTP status code `204` with an empty response.
```
-DELETE /todos
+POST /todos/mark_as_done
```
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/donmark_as_donee
```
-Example Response:
-
-```json
-3
-```
[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
diff --git a/doc/api/users.md b/doc/api/users.md
index 28b6c7bd491..14b5c6c713e 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,7 +72,6 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -105,7 +104,6 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
- "theme_id": 1,
"color_scheme_id": 3,
"projects_limit": 100,
"current_sign_in_at": "2014-03-19T17:54:13Z",
@@ -198,7 +196,6 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -216,7 +213,7 @@ Parameters:
## User creation
-Creates a new user. Note only administrators can create new users.
+Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority).
```
POST /users
@@ -225,7 +222,8 @@ POST /users
Parameters:
- `email` (required) - Email
-- `password` (required) - Password
+- `password` (optional) - Password
+- `reset_password` (optional) - Send user password reset link - true or false(default)
- `username` (required) - Username
- `name` (required) - Name
- `skype` (optional) - Skype ID
@@ -271,6 +269,7 @@ Parameters:
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
+On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
even in cases where a `409` (Conflict) would be more appropriate,
e.g. when renaming the email address to some existing one.
@@ -321,7 +320,6 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -367,7 +365,6 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -662,14 +659,14 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann
Blocks the specified user. Available only for admin.
```
-PUT /users/:id/block
+POST /users/:id/block
```
Parameters:
- `id` (required) - id of specified user
-Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
@@ -677,14 +674,14 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
Unblocks the specified user. Available only for admin.
```
-PUT /users/:id/unblock
+POST /users/:id/unblock
```
Parameters:
- `id` (required) - id of specified user
-Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
### Get user contribution events
@@ -702,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:
@@ -815,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"
},
@@ -832,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
new file mode 100644
index 00000000000..0794156bc39
--- /dev/null
+++ b/doc/api/v3_to_v4.md
@@ -0,0 +1,82 @@
+# V3 to V4 version
+
+Since GitLab 9.0, API V4 is the preferred version to be used.
+
+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).
+
+Below are the changes made between V3 and V4.
+
+### 8.17
+
+- Removed `/projects/:search` (use: `/projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `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 `projects/merge_request/:id` have been removed (use: `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 `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+
+### 9.0
+
+- Status 409 returned for POST `project/: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`
+ - `/gitlab_ci_ymls`
+ - `/dockerfiles`
+ - `/gitignores/:key`
+ - `/gitlab_ci_ymls/:key`
+ - `/dockerfiles/:key`
+- Moved `/projects/fork/:id` to `/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 `/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 ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_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 `projects/:id/issues` and `/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 `:id/repository/branches`
+ - POST `:id/repository/commits`
+ - POST/PUT/DELETE `:id/repository/files`
+- Renamed `merge when build succeeds` to merge `when pipeline succeeds parameters` 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 `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)
+- `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 `/projects/:id/repository/files?file_path=:file_path` to `/projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+ - `/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 `/projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `/projects/:id/repository/blobs/:sha?file_path=:file_path` to `/projects/:id/repository/files/:file_path/raw?ref=:sha`
+ - `/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 dd14698e9cd..d8fba5d7a77 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -2,22 +2,22 @@
## CI User documentation
-- [Get started with GitLab CI](quick_start/README.md)
+- [Getting started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
-- [Pipelines and builds](pipelines.md)
+- [Pipelines and jobs](pipelines.md)
- [Environments and deployments](environments.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
-- [Configure a Runner, the application that runs your builds](runners/README.md)
+- [Configure a Runner, the application that runs your jobs](runners/README.md)
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
- [Use CI to build Docker images](docker/using_docker_build.md)
- [CI Variables](variables/README.md) - Learn how to use variables defined in
your `.gitlab-ci.yml` or secured ones defined in your project's settings
- [Use SSH keys in your build environment](ssh_keys/README.md)
-- [Trigger builds through the API](triggers/README.md)
-- [Build artifacts](../user/project/builds/artifacts.md)
+- [Trigger jobs through the API](triggers/README.md)
+- [Job artifacts](../user/project/pipelines/job_artifacts.md)
- [User permissions](../user/permissions.md#gitlab-ci)
-- [Build permissions](../user/permissions.md#build-permissions)
+- [Jobs permissions](../user/permissions.md#jobs-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
@@ -27,6 +27,8 @@
## Breaking changes
-- [New CI build permissions model](../user/project/new_ci_build_permissions_model.md)
- Read about what changed in GitLab 8.12 and how that affects your builds.
- There's a new way to access your Git submodules and LFS objects in builds.
+- [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/autodeploy/index.md b/doc/ci/autodeploy/index.md
index c4c4d95b68a..4028a5efa9e 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -34,8 +34,8 @@ created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
-[project-services]: ../../project_services/project_services.md
+[project-services]: ../../user/project/integrations/project_services.md
[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
-[kubernetes-service]: ../../project_services/kubernetes.md
+[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md
index 05605f10fb4..22b3872025f 100644
--- a/doc/ci/build_artifacts/README.md
+++ b/doc/ci/build_artifacts/README.md
@@ -1,4 +1 @@
-This document was moved to:
-
-- [user/project/builds/artifacts.md](../../user/project/builds/artifacts.md) - user guide
-- [administration/build_artifacts.md](../../administration/build_artifacts.md) - administrator guide
+This document was moved to [pipelines/job_artifacts.md](../../user/project/pipelines/job_artifacts.md).
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index 84eaf29efd1..99669a9272a 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -1,4 +1,4 @@
# Docker integration
-+ [Using Docker Images](using_docker_images.md)
-+ [Using Docker Build](using_docker_build.md) \ No newline at end of file
+- [Using Docker Images](using_docker_images.md)
+- [Using Docker Build](using_docker_build.md)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 28141cced3b..8620984d40d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -12,6 +12,7 @@ One of the new trends in Continuous Integration/Deployment is to:
1. deploy to a server from the pushed image.
It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
+
```bash
$ docker build -t my-image dockerfiles/
$ docker run my-docker-image /script/to/run/tests
@@ -19,23 +20,23 @@ $ docker tag my-image my-registry:5000/my-image
$ docker push my-registry:5000/my-image
```
-This requires special configuration of GitLab Runner to enable `docker` support during builds.
+This requires special configuration of GitLab Runner to enable `docker` support during jobs.
## Runner Configuration
-There are three methods to enable the use of `docker build` and `docker run` during builds; each with their own tradeoffs.
+There are three methods to enable the use of `docker build` and `docker run` during jobs; each with their own tradeoffs.
### Use shell executor
The simplest approach is to install GitLab Runner in `shell` execution mode.
-GitLab Runner then executes build scripts as the `gitlab-runner` user.
+GitLab Runner then executes job scripts as the `gitlab-runner` user.
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
-1. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
+1. During GitLab Runner installation select `shell` as method of executing job scripts or use command:
```bash
- $ sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
--registration-token REGISTRATION_TOKEN \
--executor shell \
@@ -50,16 +51,17 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
3. Add `gitlab-runner` user to `docker` group:
```bash
- $ sudo usermod -aG docker gitlab-runner
+ sudo usermod -aG docker gitlab-runner
```
4. Verify that `gitlab-runner` has access to Docker:
```bash
- $ sudo -u gitlab-runner -H docker info
+ sudo -u gitlab-runner -H docker info
```
You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`:
+
```yaml
before_script:
- docker info
@@ -80,12 +82,12 @@ For more information please read [On Docker security: `docker` group considered
The second approach is to use the special docker-in-docker (dind)
[Docker image](https://hub.docker.com/_/docker/) with all tools installed
-(`docker` and `docker-compose`) and run the build script in context of that
+(`docker` and `docker-compose`) and run the job script in context of that
image in privileged mode.
In order to do that, follow the steps:
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+1. Install [GitLab Runner](https://docs.gitlab.com/runner/install).
1. Register GitLab Runner from the command line to use `docker` and `privileged`
mode:
@@ -155,10 +157,10 @@ not without its own challenges:
escalation which can lead to container breakout. For more information, check
out the official Docker documentation on
[Runtime privilege and Linux capabilities][docker-cap].
-- Using docker-in-docker, each build is in a clean environment without the past
- history. Concurrent builds work fine because every build gets it's own
+- When using docker-in-docker, each job is in a clean environment without the past
+ history. Concurrent jobs work fine because every build gets it's own
instance of Docker engine so they won't conflict with each other. But this
- also means builds can be slower because there's no caching of layers.
+ also means jobs can be slower because there's no caching of layers.
- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
form offered. To use a different driver, see
[Using the overlayfs driver](#using-the-overlayfs-driver).
@@ -171,7 +173,7 @@ The third approach is to bind-mount `/var/run/docker.sock` into the container so
In order to do that, follow the steps:
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+1. Install [GitLab Runner](https://docs.gitlab.com/runner/install).
1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
@@ -187,7 +189,9 @@ In order to do that, follow the steps:
The above command will register a new Runner to use the special
`docker:latest` image which is provided by Docker. **Notice that it's using
- the Docker daemon of the Runner itself, and any containers spawned by docker commands will be siblings of the Runner rather than children of the runner.** This may have complications and limitations that are unsuitable for your workflow.
+ the Docker daemon of the Runner itself, and any containers spawned by docker
+ commands will be siblings of the Runner rather than children of the runner.**
+ This may have complications and limitations that are unsuitable for your workflow.
The above command will create a `config.toml` entry similar to this:
@@ -206,7 +210,8 @@ In order to do that, follow the steps:
Insecure = false
```
-1. You can now use `docker` in the build script (note that you don't need to include the `docker:dind` service as when using the Docker in Docker executor):
+1. You can now use `docker` in the build script (note that you don't need to
+ include the `docker:dind` service as when using the Docker in Docker executor):
```yaml
image: docker:latest
@@ -221,18 +226,23 @@ In order to do that, follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-While the above method avoids using Docker in privileged mode, you should be aware of the following implications:
-* By sharing the docker daemon, you are effectively disabling all
-the security mechanisms of containers and exposing your host to privilege
-escalation which can lead to container breakout. For example, if a project
-ran `docker rm -f $(docker ps -a -q)` it would remove the GitLab Runner
-containers.
-* Concurrent builds may not work; if your tests
-create containers with specific names, they may conflict with each other.
-* Sharing files and directories from the source repo into containers may not
-work as expected since volume mounting is done in the context of the host
-machine, not the build container.
-e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
+While the above method avoids using Docker in privileged mode, you should be
+aware of the following implications:
+
+- By sharing the docker daemon, you are effectively disabling all
+ the security mechanisms of containers and exposing your host to privilege
+ escalation which can lead to container breakout. For example, if a project
+ ran `docker rm -f $(docker ps -a -q)` it would remove the GitLab Runner
+ containers.
+- Concurrent jobs may not work; if your tests
+ create containers with specific names, they may conflict with each other.
+- Sharing files and directories from the source repo into containers may not
+ work as expected since volume mounting is done in the context of the host
+ machine, not the build container, e.g.:
+
+ ```
+ docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
+ ```
## Using the OverlayFS driver
@@ -298,8 +308,32 @@ 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
of your Docker images.
+You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
+
+```yaml
+services:
+ - docker:dind
+
+variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+
+build:
+ stage: build
+ script:
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
+```
+
+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
+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.
+
Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
-including two tests that run in parallel. The build is stored in the container
+including two tests that run in parallel. The `build` is stored in the container
registry and used by subsequent stages, downloading the image
when needed. Changes to `master` also get tagged as `latest` and deployed using
an application-specific deploy script:
@@ -360,17 +394,17 @@ deploy:
Some things you should be aware of when using the Container Registry:
- You must log in to the container registry before running commands. Putting
- this in `before_script` will run it before each build job.
+ this in `before_script` will run it before each job.
- Using `docker build --pull` makes sure that Docker fetches any changes to base
images before building just in case your cache is stale. It takes slightly
longer, but means you don’t get stuck without security patches to base images.
- Doing an explicit `docker pull` before each `docker run` makes sure to fetch
the latest image that was just built. This is especially important if you are
using multiple runners that cache images locally. Using the git SHA in your
- image tag makes this less necessary since each build will be unique and you
+ image tag makes this less necessary since each job will be unique and you
shouldn't ever have a stale image, but it's still possible if you re-build a
given commit after a dependency has changed.
-- You don't want to build directly to `latest` in case there are multiple builds
+- You don't want to build directly to `latest` in case there are multiple jobs
happening simultaneously.
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index aba77490915..00787323b6b 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -8,7 +8,7 @@ run applications in independent "containers" that are run within a single Linux
instance. [Docker Hub][hub] has a rich database of pre-built images that can be
used to test and build your applications.
-Docker, when used with GitLab CI, runs each build in a separate and isolated
+Docker, when used with GitLab CI, runs each job in a separate and isolated
container using the predefined image that is set up in
[`.gitlab-ci.yml`](../yaml/README.md).
@@ -39,18 +39,20 @@ accessible during the build process.
## What is an image
-The `image` keyword is the name of the docker image that is present in the
-local Docker Engine (list all images with `docker images`) or any image that
-can be found at [Docker Hub][hub]. For more information about images and Docker
-Hub please read the [Docker Fundamentals][] documentation.
+The `image` keyword is the name of the docker image the docker executor
+will run to perform the CI tasks.
-In short, with `image` we refer to the docker image, which will be used to
-create a container on which your build will run.
+By default the executor will only pull images from [Docker Hub][hub],
+but this can be configured in the `gitlab-runner/config.toml` by setting
+the [docker pull policy][] to allow using local images.
+
+For more information about images and Docker Hub please read
+the [Docker Fundamentals][] documentation.
## What is a service
The `services` keyword defines just another docker image that is run during
-your build and is linked to the docker image that the `image` keyword defines.
+your job and is linked to the docker image that the `image` keyword defines.
This allows you to access the service image during build time.
The service image can run any application, but the most common use case is to
@@ -61,13 +63,13 @@ time the project is built.
You can see some widely used services examples in the relevant documentation of
[CI services examples](../services/README.md).
-### How services are linked to the build
+### How services are linked to the job
To better understand how the container linking works, read
[Linking containers together][linking-containers].
To summarize, if you add `mysql` as service to your application, the image will
-then be used to create a container that is linked to the build container.
+then be used to create a container that is linked to the job container.
The service container for MySQL will be accessible under the hostname `mysql`.
So, in order to access your database service you have to connect to the host
@@ -133,7 +135,7 @@ Look for the `[runners.docker]` section:
services = ["mysql:latest", "postgres:latest"]
```
-The image and services defined this way will be added to all builds run by
+The image and services defined this way will be added to all job run by
that runner.
## Define an image from a private Docker registry
@@ -167,7 +169,7 @@ services:
- tutum/wordpress:latest
```
-When the build is run, `tutum/wordpress` will be started and you will have
+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`.
The alias hostname for the service is made from the image name following these
@@ -202,21 +204,21 @@ See the specific documentation for
## How Docker integration works
-Below is a high level overview of the steps performed by docker during build
+Below is a high level overview of the steps performed by docker during job
time.
1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`.
1. Create cache container to store all volumes as defined in `config.toml` and
`Dockerfile` of build image (`ruby:2.1` as in above example).
1. Create build container and link any service container to build container.
-1. Start build container and send build script to the container.
-1. Run build script.
+1. Start build container and send job script to the container.
+1. Run job script.
1. Checkout code in: `/builds/group-name/project-name/`.
1. Run any step defined in `.gitlab-ci.yml`.
1. Check exit status of build script.
1. Remove build container and all created service containers.
-## How to debug a build locally
+## How to debug a job locally
*Note: The following commands are run without root privileges. You should be
able to run docker with your regular user account.*
@@ -271,6 +273,7 @@ containers as well as all volumes (`-v`) that were created with the container
creation.
[Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/
+[docker pull policy]: https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work
[hub]: https://hub.docker.com/
[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
[tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index c10f82054e2..796a025b951 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -11,10 +11,10 @@ API.
---
-As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a
-project. Disabling GitLab CI in a project does not delete any previous builds.
-In fact, the `/builds` page can still be accessed, although it's hidden from
-the left sidebar menu.
+GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project.
+Disabling GitLab CI in a project does not delete any previous jobs.
+In fact, the `/pipelines` and `/builds` pages can still be accessed, although
+it's hidden from the left sidebar menu.
GitLab CI is enabled by default on new installations and can be disabled either
individually under each project's settings, or site-wide by modifying the
@@ -23,12 +23,12 @@ respectively.
### Per-project user setting
-The setting to enable or disable GitLab CI can be found with the name **Builds**
-under the **Features** area of a project's settings along with **Issues**,
-**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox
-and hit **Save** for the settings to take effect.
+The setting to enable or disable GitLab CI can be found with the name **Pipelines**
+under the **Sharing & Permissions** area of a project's settings along with
+**Merge Requests**. Choose one of **Disabled**, **Only team members** and
+**Everyone with access** and hit **Save changes** for the settings to take effect.
-![Features settings](img/features_settings.png)
+![Sharing & Permissions settings](img/permissions_settings.png)
---
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index ef04c537367..3c31ba45d3d 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -1,7 +1,6 @@
# Introduction to environments and deployments
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
During the development of software, there can be many stages until it's ready
for public consumption. You sure want to first test your code and then deploy it
@@ -76,7 +75,7 @@ We have defined 3 [stages](yaml/README.md#stages):
- deploy
The jobs assigned to these stages will run in this order. If a job fails, then
-the builds that are assigned to the next stage won't run, rendering the pipeline
+the jobs that are assigned to the next stage won't run, rendering the pipeline
as failed. In our case, the `test` job will run first, then the `build` and
lastly the `deploy_staging`. With this, we ensure that first the tests pass,
then our app is able to be built successfully, and lastly we deploy to the
@@ -120,7 +119,7 @@ There's a bunch of information there, specifically you can see:
- The environment's name with a link to its deployments
- The last deployment ID number and who performed it
-- The build ID of the last deployment with its respective job name
+- The job ID of the last deployment with its respective job name
- The commit information of the last deployment such as who committed, to what
branch and the Git SHA of the commit
- The exact time the last deployment was performed
@@ -220,9 +219,9 @@ deploy_prod:
The `when: manual` action exposes a play button in GitLab's UI and the
`deploy_prod` job will only be triggered if and when we click that play button.
-You can find it in the pipeline, build, environment, and deployment views.
+You can find it in the pipeline, job, environment, and deployment views.
-| Pipelines | Single pipeline | Environments | Deployments | Builds |
+| Pipelines | Single pipeline | Environments | Deployments | jobs |
| --------- | ----------------| ------------ | ----------- | -------|
| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
@@ -242,7 +241,7 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open
+the [Kubernetes service][kubernetes-service], GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service documentation.
@@ -420,7 +419,7 @@ Behind the scenes:
- GitLab Runner picks up the changes and starts running the jobs
- The jobs run sequentially as defined in `stages`
- First, the tests pass
- - Then, the build begins and successfully also passes
+ - Then, the job begins and successfully also passes
- Lastly, the app is deployed to an environment with a name specific to the
branch
@@ -443,6 +442,57 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
+### Go directly from source files to public pages on the environment
+
+> Introduced in GitLab 8.17.
+
+To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places:
+
+| In the diff for a merge request, comparison or commit | In the file view |
+| ------ | ------ |
+| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) |
+
+To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map.
+
+A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website).
+
+This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com):
+
+```yaml
+# Team data
+- source: 'data/team.yml' # data/team.yml
+ public: 'team/' # team/
+
+# Blogposts
+- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+# HTML files
+- source: /source\/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+# Other files
+- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+```
+
+Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys:
+
+- `source`
+ - a string, starting and ending with `'`, for an exact match
+ - a regular expression, starting and ending with `/`, for a pattern match
+ - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
+ - Can include capture groups denoted by `()` that can be referred to in the `public` path.
+ - Slashes (`/`) can, but don't have to, be escaped as `\/`.
+ - Literal periods (`.`) should be escaped as `\.`.
+- `public`
+ - a string, starting and ending with `'`.
+ - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`.
+
+The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate.
+
+In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`.
+
---
We now have a full development cycle, where our app is tested, built, deployed
@@ -485,6 +535,7 @@ deploy_review:
- master
stop_review:
+ stage: deploy
variables:
GIT_STRATEGY: none
script:
@@ -505,7 +556,9 @@ when their associated branch is deleted.
When you have an environment that has a stop action defined (typically when
the environment describes a review app), GitLab will automatically trigger a
-stop action when the associated branch is deleted.
+stop action when the associated branch is deleted. The `stop_review` job must
+be in the same `stage` as the `deploy_review` one in order for the environment
+to automatically stop.
You can read more in the [`.gitlab-ci.yml` reference][onstop].
@@ -566,7 +619,7 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
-[kubernetes-service]: ../project_services/kubernetes.md]
+[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index 7d91ce6710f..d28aa282825 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -91,7 +91,7 @@ Secure Variables can added by going to `Project > Variables > Add Variable`.
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
The variables that are defined in the project settings are sent along with the build script to the runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
-It is also important that secret's value is hidden in the build log.
+It is also important that secret's value is hidden in the job log.
You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners):
1. `$SECRET_VARIABLE` - use it for non-Windows runners
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
index 5334a73e1f5..8b0d8a003fd 100644
--- a/doc/ci/examples/deployment/composer-npm-deploy.md
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -65,7 +65,7 @@ In order, this means that:
1. We check if the `ssh-agent` is available and we install it if it's not;
2. We create the `~/.ssh` folder;
3. We make sure we're running bash;
-4. We disable host checking (we don't ask for user accept when we first connect to a server; and since every build will equal a first connect, we kind of need this)
+4. We disable host checking (we don't ask for user accept when we first connect to a server; and since every job will equal a first connect, we kind of need this)
And this is basically all you need in the `before_script` section.
@@ -153,4 +153,4 @@ stage_deploy:
- scp -P22 -r build/* server_user@server_host:htdocs/wp-content/themes/_tmp
- ssh -p22 server_user@server_host "mv htdocs/wp-content/themes/live htdocs/wp-content/themes/_old && mv htdocs/wp-content/themes/_tmp htdocs/wp-content/themes/live"
- ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
-``` \ No newline at end of file
+```
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 5eeec92d976..f2dd12b67d3 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -15,10 +15,10 @@ This will allow us to test PHP projects against different versions of PHP.
However, not everything is plug 'n' play, you still need to configure some
things manually.
-As with every build, you need to create a valid `.gitlab-ci.yml` describing the
+As with every job, you need to create a valid `.gitlab-ci.yml` describing the
build environment.
-Let's first specify the PHP image that will be used for the build process
+Let's first specify the PHP image that will be used for the job process
(you can read more about what an image means in the Runner's lingo reading
about [Using Docker images](../docker/using_docker_images.md#what-is-image)).
@@ -58,8 +58,8 @@ docker-php-ext-install pdo_mysql
```
You might wonder what `docker-php-ext-install` is. In short, it is a script
-provided by the official php docker image that you can use to easilly install
-extensions. For more information read the the documentation at
+provided by the official php docker image that you can use to easily install
+extensions. For more information read the documentation at
<https://hub.docker.com/r/_/php/>.
Now that we created the script that contains all prerequisites for our build
@@ -142,7 +142,7 @@ Of course, `my_php.ini` must be present in the root directory of your repository
## Test PHP projects using the Shell executor
-The shell executor runs your builds in a terminal session on your server.
+The shell executor runs your job in a terminal session on your server.
Thus, in order to test your projects you first need to make sure that all
dependencies are installed.
@@ -280,7 +280,7 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
-moments the changes will be picked by a public runner and the build will begin.
+moments the changes will be picked by a public runner and the job will begin.
[php-hub]: https://hub.docker.com/r/_/php/
[phpenv]: https://github.com/phpenv/phpenv
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 85f8849fa99..01c13941c21 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -51,14 +51,14 @@ The `deploy` stage automatically deploys the project to Heroku using dpl.
You can use other versions of Scala and SBT by defining them in
`build.sbt`.
-## Display test coverage in build
+## Display test coverage in job
Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
-**Settings ➔ Edit Project ➔ Test coverage parsing** project setting to
+**Settings ➔ CI/CD Pipelines ➔ Coverage report** project setting to
retrieve the [test coverage] rate from the build trace and have it
-displayed with your builds.
+displayed with your jobs.
-**Builds** must be enabled for this option to appear.
+**Pipelines** must be enabled for this option to appear.
## Heroku application
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 869743ce80a..36c6e153d95 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -1,14 +1,14 @@
# Using Git submodules with GitLab CI
> **Notes:**
-- GitLab 8.12 introduced a new [CI build permissions model][newperms] and you
+- GitLab 8.12 introduced a new [CI job permissions model][newperms] and you
are encouraged to upgrade your GitLab instance if you haven't done already.
If you are **not** using GitLab 8.12 or higher, you would need to work your way
around submodules in order to access the sources of e.g., `gitlab.com/group/project`
with the use of [SSH keys](ssh_keys/README.md).
-- With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+- With GitLab 8.12 onward, your permissions are used to evaluate what a CI job
can access. More information about how this system works can be found in the
- [Build permissions model](../user/permissions.md#builds-permissions).
+ [Jobs permissions model](../user/permissions.md#jobs-permissions).
- The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance.
## Configuring the `.gitmodules` file
@@ -27,7 +27,7 @@ Let's consider the following example:
If you are using GitLab 8.12+ and your submodule is on the same GitLab server,
you must update your `.gitmodules` file to use **relative URLs**.
Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
-this easily allows you to use HTTP(S) for cloning all your CI builds and SSH
+this easily allows you to use HTTP(S) for cloning all your CI jobs and SSH
for all your local checkouts. The `.gitmodules` would look like:
```ini
@@ -38,7 +38,7 @@ for all your local checkouts. The `.gitmodules` would look like:
The above configuration will instruct Git to automatically deduce the URL that
should be used when cloning sources. Whether you use HTTP(S) or SSH, Git will use
-that same channel and it will allow to make all your CI builds use HTTP(S)
+that same channel and it will allow to make all your CI jobs use HTTP(S)
(because GitLab CI only uses HTTP(S) for cloning your sources), and all your local
clones will continue using SSH.
@@ -57,13 +57,13 @@ Once `.gitmodules` is correctly configured, you can move on to
## Using Git submodules in your CI jobs
There are a few steps you need to take in order to make submodules work
-correctly with your CI builds:
+correctly with your CI jobs:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
`GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
- the runner to fetch your submodules before the build:
+ the runner to fetch your submodules before the job:
```yaml
variables:
GIT_SUBMODULE_STRATEGY: recursive
@@ -87,9 +87,9 @@ The rationale to set the `sync` and `update` in `before_script` is because of
the way Git submodules work. On a fresh Runner workspace, Git will set the
submodule URL including the token in `.git/config`
(or `.git/modules/<submodule>/config`) based on `.gitmodules` and the current
-remote URL. On subsequent builds on the same Runner, `.git/config` is cached
+remote URL. On subsequent jobs on the same Runner, `.git/config` is cached
and already contains a full URL for the submodule, corresponding to the previous
-build, and to **a token from a previous build**. `sync` allows to force updating
+job, and to **a token from a previous job**. `sync` allows to force updating
the full URL.
[gitpro]: ../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png
deleted file mode 100644
index c159253d1c9..00000000000
--- a/doc/ci/img/features_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/permissions_settings.png b/doc/ci/img/permissions_settings.png
new file mode 100644
index 00000000000..1454c75fd24
--- /dev/null
+++ b/doc/ci/img/permissions_settings.png
Binary files differ
diff --git a/doc/ci/img/pipelines-goal.svg b/doc/ci/img/pipelines-goal.svg
new file mode 100644
index 00000000000..a925e2282a4
--- /dev/null
+++ b/doc/ci/img/pipelines-goal.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" standalone="yes"?>
+
+<svg version="1.1" viewBox="0.0 0.0 1091.020997375328 262.04461942257217" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="p.0"><path d="m0 0l1091.021 0l0 262.04462l-1091.021 0l0 -262.04462z" clip-rule="nonzero"></path></clipPath><g clip-path="url(#p.0)"><path fill="#000000" fill-opacity="0.0" d="m0 0l1091.021 0l0 262.04462l-1091.021 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m226.93439 3.7664042l860.7559 0l0 249.5748l-860.7559 0z" fill-rule="nonzero"></path><path stroke="#666666" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="4.0,3.0" d="m226.93439 3.7664042l860.7559 0l0 249.5748l-860.7559 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m67.72179 27.199474l147.2126 0l0 39.464565l-147.2126 0z" fill-rule="nonzero"></path><path fill="#000000" d="m126.91313 49.353848l1.796875 0.453125q-0.5625 2.21875 -2.03125 3.390625q-1.46875 1.15625 -3.59375 1.15625q-2.203125 0 -3.578125 -0.890625q-1.375 -0.90625 -2.09375 -2.59375q-0.71875 -1.703125 -0.71875 -3.65625q0 -2.125 0.796875 -3.703125q0.8125 -1.578125 2.3125 -2.390625q1.5 -0.828125 3.296875 -0.828125q2.046875 0 3.4375 1.046875q1.390625 1.03125 1.9375 2.90625l-1.765625 0.421875q-0.46875 -1.484375 -1.375 -2.15625q-0.90625 -0.6875 -2.265625 -0.6875q-1.5625 0 -2.625 0.75q-1.046875 0.75 -1.484375 2.03125q-0.421875 1.265625 -0.421875 2.609375q0 1.734375 0.5 3.03125q0.515625 1.28125 1.578125 1.921875q1.078125 0.640625 2.3125 0.640625q1.515625 0 2.5625 -0.859375q1.046875 -0.875 1.421875 -2.59375zm3.5354462 4.765625l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.978302 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm15.547592 4.65625q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.735092 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277054 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm14.449646 5.875l0 -13.59375l2.71875 0l3.21875 9.625q0.4375 1.34375 0.640625 2.015625q0.234375 -0.75 0.734375 -2.1875l3.25 -9.453125l2.421875 0l0 13.59375l-1.734375 0l0 -11.390625l-3.953125 11.390625l-1.625 0l-3.9375 -11.578125l0 11.578125l-1.734375 0zm15.634552 0l0 -13.59375l6.03125 0q1.8125 0 2.75 0.359375q0.953125 0.359375 1.515625 1.296875q0.5625 0.921875 0.5625 2.046875q0 1.453125 -0.9375 2.453125q-0.921875 0.984375 -2.890625 1.25q0.71875 0.34375 1.09375 0.671875q0.78125 0.734375 1.484375 1.8125l2.375 3.703125l-2.265625 0l-1.796875 -2.828125q-0.796875 -1.21875 -1.3125 -1.875q-0.5 -0.65625 -0.90625 -0.90625q-0.40625 -0.265625 -0.8125 -0.359375q-0.3125 -0.078125 -1.015625 -0.078125l-2.078125 0l0 6.046875l-1.796875 0zm1.796875 -7.59375l3.859375 0q1.234375 0 1.921875 -0.25q0.703125 -0.265625 1.0625 -0.828125q0.375 -0.5625 0.375 -1.21875q0 -0.96875 -0.703125 -1.578125q-0.703125 -0.625 -2.21875 -0.625l-4.296875 0l0 4.5z" fill-rule="nonzero"></path><path fill="#efefef" d="m765.3307 106.94125l147.21265 0l0 59.74803l-147.21265 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m765.3307 106.94125l147.21265 0l0 59.74803l-147.21265 0z" fill-rule="nonzero"></path><path fill="#000000" d="m800.99805 132.73526l0 -13.593742l4.6875 0q1.578125 0 2.421875 0.1875q1.15625 0.265625 1.984375 0.96875q1.078125 0.921875 1.609375 2.34375q0.53125 1.40625 0.53125 3.21875q0 1.546875 -0.359375 2.7499924q-0.359375 1.1875 -0.921875 1.984375q-0.5625 0.78125 -1.234375 1.234375q-0.671875 0.4375 -1.625 0.671875q-0.953125 0.234375 -2.1875 0.234375l-4.90625 0zm1.796875 -1.609375l2.90625 0q1.34375 0 2.109375 -0.25q0.765625 -0.25 1.21875 -0.703125q0.640625 -0.640625 1.0 -1.71875q0.359375 -1.0781174 0.359375 -2.6249924q0 -2.125 -0.703125 -3.265625q-0.703125 -1.15625 -1.703125 -1.546875q-0.71875 -0.28125 -2.328125 -0.28125l-2.859375 0l0 10.390617zm18.207336 -1.5625l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.7343674q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.43749237l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.7031174l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.110107 9.656242l0 -13.640617l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.5156174 -0.546875 2.7343674q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.656242q0 1.9062424 0.765625 2.8124924q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.9218674q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm8.828857 4.8749924l0 -13.593742l1.671875 0l0 13.593742l-1.671875 0zm3.5510254 -4.9218674q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.9062424 -0.578125 2.9999924q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8124924zm1.71875 0q0 1.8906174 0.828125 2.8281174q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.8906174q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.203857 8.718742l-0.171875 -1.5625q0.546875 0.140625 0.953125 0.140625q0.546875 0 0.875 -0.1875q0.34375 -0.1875 0.5625 -0.515625q0.15625 -0.25 0.5 -1.25q0.046875 -0.140625 0.15625 -0.40625l-3.734375 -9.874992l1.796875 0l2.046875 5.7187424q0.40625 1.078125 0.71875 2.28125q0.28125 -1.15625 0.6875 -2.25l2.09375 -5.7499924l1.671875 0l-3.75 10.031242q-0.59375 1.625 -0.9375 2.234375q-0.4375 0.828125 -1.015625 1.203125q-0.578125 0.390625 -1.375 0.390625q-0.484375 0 -1.078125 -0.203125zm18.245789 -5.296875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.6562424l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.7499924q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.9020996 -3.4218674q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.9062424 -0.578125 2.9999924q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8124924zm1.71875 0q0 1.8906174 0.828125 2.8281174q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.8906174q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125z" fill-rule="nonzero"></path><path fill="#000000" d="m808.1591 150.36026l1.6875 -0.140625q0.125 1.015625 0.5625 1.671875q0.4375 0.65625 1.359375 1.0625q0.9375 0.40625 2.09375 0.40625q1.03125 0 1.8125 -0.3125q0.796875 -0.3125 1.1875 -0.84375q0.390625 -0.53125 0.390625 -1.15625q0 -0.640625 -0.375 -1.109375q-0.375 -0.484375 -1.234375 -0.8125q-0.546875 -0.21875 -2.421875 -0.65625q-1.875 -0.453125 -2.625 -0.859375q-0.96875 -0.515625 -1.453125 -1.265625q-0.46875 -0.75 -0.46875 -1.6875q0 -1.03125 0.578125 -1.921875q0.59375 -0.90625 1.703125 -1.359375q1.125 -0.46875 2.5 -0.46875q1.515625 0 2.671875 0.484375q1.15625 0.484375 1.765625 1.4375q0.625 0.9375 0.671875 2.140625l-1.71875 0.125q-0.140625 -1.28125 -0.953125 -1.9375q-0.796875 -0.671875 -2.359375 -0.671875q-1.625 0 -2.375 0.609375q-0.75 0.59375 -0.75 1.4375q0 0.734375 0.53125 1.203125q0.515625 0.46875 2.703125 0.96875q2.203125 0.5 3.015625 0.875q1.1875 0.546875 1.75 1.390625q0.578125 0.828125 0.578125 1.921875q0 1.09375 -0.625 2.0625q-0.625 0.953125 -1.796875 1.484375q-1.15625 0.53125 -2.609375 0.53125q-1.84375 0 -3.09375 -0.53125q-1.25 -0.546875 -1.96875 -1.625q-0.703125 -1.078125 -0.734375 -2.453125zm16.490417 2.875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm7.9645386 0.28125q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm3.7819824 5.75l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.313232 -6.578125l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm10.078796 0.8125l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875z" fill-rule="nonzero"></path><path fill="#efefef" d="m925.54333 177.39108l147.21252 0l0 59.74803l-147.21252 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m925.54333 177.39108l147.21252 0l0 59.74803l-147.21252 0z" fill-rule="nonzero"></path><path fill="#000000" d="m961.2107 203.18509l0 -13.59375l4.6875 0q1.578125 0 2.421875 0.1875q1.15625 0.265625 1.984375 0.96875q1.078125 0.921875 1.609375 2.34375q0.53125 1.40625 0.53125 3.21875q0 1.546875 -0.359375 2.75q-0.359375 1.1875 -0.921875 1.984375q-0.5625 0.78125 -1.234375 1.234375q-0.671875 0.4375 -1.625 0.671875q-0.953125 0.234375 -2.1875 0.234375l-4.90625 0zm1.796875 -1.609375l2.90625 0q1.34375 0 2.109375 -0.25q0.765625 -0.25 1.21875 -0.703125q0.640625 -0.640625 1.0 -1.71875q0.359375 -1.078125 0.359375 -2.625q0 -2.125 -0.703125 -3.265625q-0.703125 -1.15625 -1.703125 -1.546875q-0.71875 -0.28125 -2.328125 -0.28125l-2.859375 0l0 10.390625zm18.207275 -1.5625l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.110107 9.65625l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm8.828857 4.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm3.5510864 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.203796 8.71875l-0.171875 -1.5625q0.546875 0.140625 0.953125 0.140625q0.546875 0 0.875 -0.1875q0.34375 -0.1875 0.5625 -0.515625q0.15625 -0.25 0.5 -1.25q0.046875 -0.140625 0.15625 -0.40625l-3.734375 -9.875l1.796875 0l2.046875 5.71875q0.40625 1.078125 0.71875 2.28125q0.28125 -1.15625 0.6875 -2.25l2.09375 -5.75l1.671875 0l-3.75 10.03125q-0.59375 1.625 -0.9375 2.234375q-0.4375 0.828125 -1.015625 1.203125q-0.578125 0.390625 -1.375 0.390625q-0.484375 0 -1.078125 -0.203125zm18.24585 -5.296875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.90197754 -3.421875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125z" fill-rule="nonzero"></path><path fill="#000000" d="m956.0228 225.18509l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.4122925 7.140625l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.6033325 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672607 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm15.719421 4.921875l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm10.360107 -3.609375l1.640625 0.21875q-0.265625 1.6875 -1.375 2.65625q-1.109375 0.953125 -2.734375 0.953125q-2.015625 0 -3.25 -1.3125q-1.21875 -1.328125 -1.21875 -3.796875q0 -1.59375 0.515625 -2.78125q0.53125 -1.203125 1.609375 -1.796875q1.09375 -0.609375 2.359375 -0.609375q1.609375 0 2.625 0.8125q1.015625 0.8125 1.3125 2.3125l-1.625 0.25q-0.234375 -1.0 -0.828125 -1.5q-0.59375 -0.5 -1.421875 -0.5q-1.265625 0 -2.0625 0.90625q-0.78125 0.90625 -0.78125 2.859375q0 1.984375 0.765625 2.890625q0.765625 0.890625 1.984375 0.890625q0.984375 0 1.640625 -0.59375q0.65625 -0.609375 0.84375 -1.859375zm6.546875 2.109375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.531311 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.281311 -1.328125 -1.281311 -3.8125zm1.718811 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.28186 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m57.989502 117.082985l156.94489 0l0 39.46456l-156.94489 0z" fill-rule="nonzero"></path><path fill="#000000" d="m71.518036 144.00298l0 -13.59375l2.71875 0l3.21875 9.625q0.4375 1.34375 0.640625 2.015625q0.234375 -0.75 0.734375 -2.1875l3.25 -9.453125l2.421875 0l0 13.59375l-1.734375 0l0 -11.390625l-3.953125 11.390625l-1.625 0l-3.9375 -11.578125l0 11.578125l-1.734375 0zm22.134552 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.094467 5.875l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.931427 0.8125l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm16.047592 1.9375l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm17.949646 4.375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.90205383 -3.421875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm14.621521 4.921875l0 -13.59375l2.71875 0l3.21875 9.625q0.4375 1.34375 0.640625 2.015625q0.234375 -0.75 0.734375 -2.1875l3.25 -9.453125l2.421875 0l0 13.59375l-1.734375 0l0 -11.390625l-3.953125 11.390625l-1.625 0l-3.9375 -11.578125l0 11.578125l-1.734375 0zm21.822052 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm3.4069672 2.0l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277054 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.094467 5.875l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m5.0104985 187.5328l209.95276 0l0 39.46457l-209.95276 0z" fill-rule="nonzero"></path><path fill="#000000" d="m34.188858 214.4528l0 -13.59375l2.71875 0l3.21875 9.625q0.4375 1.34375 0.640625 2.015625q0.234375 -0.75 0.734375 -2.1875l3.25 -9.453125l2.421875 0l0 13.59375l-1.734375 0l0 -11.390625l-3.953125 11.390625l-1.625 0l-3.9375 -11.578125l0 11.578125l-1.734375 0zm22.134552 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.094467 5.875l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.931427 0.8125l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm16.047592 1.9375l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm17.949646 4.375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.90205383 -3.421875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm14.684021 4.921875l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.412323 7.140625l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.603302 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672592 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm15.719467 4.921875l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm10.360092 -3.609375l1.640625 0.21875q-0.265625 1.6875 -1.375 2.65625q-1.109375 0.953125 -2.734375 0.953125q-2.015625 0 -3.25 -1.3125q-1.21875 -1.328125 -1.21875 -3.796875q0 -1.59375 0.515625 -2.78125q0.53125 -1.203125 1.609375 -1.796875q1.09375 -0.609375 2.359375 -0.609375q1.609375 0 2.625 0.8125q1.015625 0.8125 1.3125 2.3125l-1.625 0.25q-0.234375 -1.0 -0.828125 -1.5q-0.59375 -0.5 -1.421875 -0.5q-1.265625 0 -2.0625 0.90625q-0.78125 0.90625 -0.78125 2.859375q0 1.984375 0.765625 2.890625q0.765625 0.890625 1.984375 0.890625q0.984375 0 1.640625 -0.59375q0.65625 -0.609375 0.84375 -1.859375zm6.546875 2.109375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426788 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5041962 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281967 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm10.813217 0l0 -1.90625l1.90625 0l0 1.90625q0 1.046875 -0.375 1.6875q-0.375 0.65625 -1.171875 1.0l-0.46875 -0.71875q0.53125 -0.21875 0.78125 -0.671875q0.25 -0.453125 0.28125 -1.296875l-0.953125 0z" fill-rule="nonzero"></path><path fill="#000000" d="m51.19565 236.4528l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.161606 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm3.7819672 5.75l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm14.887146 5.109375l0 -8.546875l-1.484375 0l0 -1.3125l1.484375 0l0 -1.046875q0 -0.984375 0.171875 -1.46875q0.234375 -0.65625 0.84375 -1.046875q0.609375 -0.40625 1.703125 -0.40625q0.703125 0 1.5625 0.15625l-0.25 1.46875q-0.515625 -0.09375 -0.984375 -0.09375q-0.765625 0 -1.078125 0.328125q-0.3125 0.3125 -0.3125 1.203125l0 0.90625l1.921875 0l0 1.3125l-1.921875 0l0 8.546875l-1.65625 0zm4.152054 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.266342 4.921875l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm11.661606 0l0 -13.59375l6.03125 0q1.8125 0 2.75 0.359375q0.953125 0.359375 1.515625 1.296875q0.5625 0.921875 0.5625 2.046875q0 1.453125 -0.9375 2.453125q-0.921875 0.984375 -2.890625 1.25q0.71875 0.34375 1.09375 0.671875q0.78125 0.734375 1.484375 1.8125l2.375 3.703125l-2.265625 0l-1.796875 -2.828125q-0.796875 -1.21875 -1.3125 -1.875q-0.5 -0.65625 -0.90625 -0.90625q-0.40625 -0.265625 -0.8125 -0.359375q-0.3125 -0.078125 -1.015625 -0.078125l-2.078125 0l0 6.046875l-1.796875 0zm1.796875 -7.59375l3.859375 0q1.234375 0 1.921875 -0.25q0.703125 -0.265625 1.0625 -0.828125q0.375 -0.5625 0.375 -1.21875q0 -0.96875 -0.703125 -1.578125q-0.703125 -0.625 -2.21875 -0.625l-4.296875 0l0 4.5zm18.176071 4.421875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078842 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.926071 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm15.547592 4.65625q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm3.4069672 2.0l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm16.75 -0.234375l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.547592 5.875l0 -1.90625l1.90625 0l0 1.90625q0 1.046875 -0.375 1.6875q-0.375 0.65625 -1.171875 1.0l-0.46875 -0.71875q0.53125 -0.21875 0.78125 -0.671875q0.25 -0.453125 0.28125 -1.296875l-0.953125 0zm9.304108 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.266342 4.921875l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m88.11708 258.45282l0 -13.593765l2.71875 0l3.21875 9.625q0.4375 1.34375 0.640625 2.0156403q0.234375 -0.75001526 0.734375 -2.1875153l3.25 -9.453125l2.421875 0l0 13.593765l-1.734375 0l0 -11.39064l-3.953125 11.39064l-1.625 0l-3.9375 -11.57814l0 11.57814l-1.734375 0zm21.822052 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.0312653q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.3281403 0.09375 2.9531403q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.7187653q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.67189026 0.5 1.1250153q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.1562653q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm4.078842 4.9375153l0 -9.85939l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625153l-1.671875 0l0 -6.0000153q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.3750153l-1.671875 0zm16.828842 0l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.50001526 -0.09375 -1.5625153l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65626526 0.671875 1.0312653q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.0468903q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.85939l-1.5 0zm10.360092 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.0312653q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.3281403 0.09375 2.9531403q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.7187653q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.67189026 0.5 1.1250153q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.1562653q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm4.047592 4.9375153l0 -13.593765l1.671875 0l0 13.593765l-1.671875 0zm13.015625 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.9843903l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71876526 0.078125 0.92189026q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5114288 1.5l0 -9.85939l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.1562653l-1.671875 0zm6.243927 -11.687515l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.687515l0 -9.85939l1.671875 0l0 9.85939l-1.671875 0zm3.8323212 0.8125l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.4687653 -1.109375 -3.5156403q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.51564q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.9218903q0 1.953125 0.765625 2.8437653q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.89064026 0.78125 -2.7812653q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.000717 5.9218903l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.4687653 -1.109375 -3.5156403q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.51564q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.9218903q0 1.953125 0.765625 2.8437653q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.89064026 0.78125 -2.7812653q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm16.047592 1.9375l1.71875 0.21875q-0.40625 1.5000153 -1.515625 2.3437653q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.7343903q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.4843903q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.5468903zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.094467 5.8750153l0 -9.85939l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.1562653l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m214.93439 46.93176l13.599762 0l0 0.062992096l13.612839 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m214.93439 46.93176l13.599747 0l0 0.062992096l7.612854 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m236.14699 48.646484l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#efefef" d="m242.13387 17.057743l147.2126 0l0 59.748028l-147.2126 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m242.13387 17.057743l147.2126 0l0 59.748028l-147.2126 0z" fill-rule="nonzero"></path><path fill="#000000" d="m296.38846 53.851757l0 -13.59375l5.109375 0q1.546875 0 2.484375 0.40625q0.953125 0.40625 1.484375 1.265625q0.53125 0.859375 0.53125 1.796875q0 0.875 -0.46875 1.65625q-0.46875 0.765625 -1.4375 1.234375q1.234375 0.359375 1.890625 1.234375q0.671875 0.875 0.671875 2.0625q0 0.953125 -0.40625 1.78125q-0.390625 0.8125 -0.984375 1.265625q-0.59375 0.4375 -1.5 0.671875q-0.890625 0.21875 -2.1875 0.21875l-5.1875 0zm1.796875 -7.890625l2.9375 0q1.203125 0 1.71875 -0.15625q0.6875 -0.203125 1.03125 -0.671875q0.359375 -0.46875 0.359375 -1.1875q0 -0.671875 -0.328125 -1.1875q-0.328125 -0.515625 -0.9375 -0.703125q-0.59375 -0.203125 -2.0625 -0.203125l-2.71875 0l0 4.109375zm0 6.28125l3.390625 0q0.875 0 1.21875 -0.0625q0.625 -0.109375 1.046875 -0.359375q0.421875 -0.265625 0.6875 -0.765625q0.265625 -0.5 0.265625 -1.140625q0 -0.765625 -0.390625 -1.328125q-0.390625 -0.5625 -1.078125 -0.78125q-0.6875 -0.234375 -1.984375 -0.234375l-3.15625 0l0 4.671875zm16.959198 1.609375l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm3.9382324 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.097931 0l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.566711 0l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375z" fill-rule="nonzero"></path><path fill="#efefef" d="m408.5328 17.057743l147.21262 0l0 59.748028l-147.21262 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m408.5328 17.057743l147.21262 0l0 59.748028l-147.21262 0z" fill-rule="nonzero"></path><path fill="#000000" d="m455.20813 40.258007l1.796875 0l0 7.84375q0 2.0625 -0.46875 3.265625q-0.453125 1.203125 -1.671875 1.96875q-1.203125 0.75 -3.171875 0.75q-1.90625 0 -3.125 -0.65625q-1.21875 -0.65625 -1.734375 -1.90625q-0.515625 -1.25 -0.515625 -3.421875l0 -7.84375l1.796875 0l0 7.84375q0 1.765625 0.328125 2.609375q0.328125 0.84375 1.125 1.296875q0.8125 0.453125 1.96875 0.453125q1.984375 0 2.828125 -0.890625q0.84375 -0.90625 0.84375 -3.46875l0 -7.84375zm4.332306 13.59375l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm10.391357 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm7.785431 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm10.382233 1.5l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438202 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m594.1181 37.30971l147.21259 0l0 59.748028l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m594.1181 37.30971l147.21259 0l0 59.748028l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m625.4092 63.103725l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.6676636 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031982 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277039 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813232 6.6875l1.609375 0.24999619q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.2812462q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3124962 -0.46875 3.2656212q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.3906212zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6657715 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351074 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m654.5047 85.10372l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438232 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m584.5302 27.199474l147.21259 0l0 59.748035l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m584.5302 27.199474l147.21259 0l0 59.748035l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m615.8212 52.99349l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.6677246 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031921 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.2771 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813171 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6658325 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7350464 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5427246 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5041504 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m644.9168 74.99349l0 -12.000004l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.000004l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438171 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375038l1.65625 -1.0l0 3.4375038l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m574.93176 17.057743l147.21259 0l0 59.748028l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m574.93176 17.057743l147.21259 0l0 59.748028l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m606.22284 42.851757l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.6676636 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031982 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277039 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813232 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6657715 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351074 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m635.31836 64.85175l0 -11.999996l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 11.999996l-1.796875 0zm14.474121 -3.1718712l1.71875 0.21875q-0.40625 1.5 -1.515625 2.3437462q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.3281212q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438232 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.7343712 -1.40625 1.1406212q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.7968712 -1.28125 -2.3593712zm13.65625 1.4375l0.234375 1.4843712q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.7499962q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m389.34647 46.93176l9.593231 0l0 0.062992096l9.58786 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m389.34647 46.93176l9.593201 0l0 0.062992096l3.5878906 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m402.52756 48.646484l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m555.7454 46.93176l9.593201 0l0 0.062992096l9.587891 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m555.7454 46.93176l9.593201 0l0 0.062992096l3.5878906 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m568.9265 48.646484l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m214.96326 207.26509l355.29132 0l0 0.06298828l355.29138 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m214.96326 207.26509l355.29132 0l0 0.06298828l349.29138 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m919.54596 208.97981l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m214.93439 136.81526l14.661118 0l0 0.06300354l14.661713 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m214.93439 136.81526l14.661133 0l0 0.06300354l8.661697 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m238.25722 138.53l4.538101 -1.6517334l-4.538101 -1.6517334z" fill-rule="evenodd"></path><path fill="#efefef" d="m244.25691 106.94125l147.2126 0l0 59.74803l-147.2126 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m244.25691 106.94125l147.2126 0l0 59.74803l-147.2126 0z" fill-rule="nonzero"></path><path fill="#000000" d="m298.5115 143.73526l0 -13.59375l5.109375 0q1.546875 0 2.484375 0.40625q0.953125 0.40625 1.484375 1.265625q0.53125 0.859375 0.53125 1.796875q0 0.875 -0.46875 1.65625q-0.46875 0.765625 -1.4375 1.234375q1.234375 0.359375 1.890625 1.234375q0.671875 0.875 0.671875 2.0625q0 0.953125 -0.40625 1.78125q-0.390625 0.8125 -0.984375 1.265625q-0.59375 0.4375 -1.5 0.671875q-0.890625 0.21875 -2.1875 0.21875l-5.1875 0zm1.796875 -7.890625l2.9375 0q1.203125 0 1.71875 -0.15625q0.6875 -0.203125 1.03125 -0.671875q0.359375 -0.46875 0.359375 -1.1875q0 -0.671875 -0.328125 -1.1875q-0.328125 -0.515625 -0.9375 -0.703125q-0.59375 -0.203125 -2.0625 -0.203125l-2.71875 0l0 4.109375zm0 6.28125l3.390625 0q0.875 0 1.21875 -0.0625q0.625 -0.109375 1.046875 -0.359375q0.421875 -0.265625 0.6875 -0.765625q0.265625 -0.5 0.265625 -1.140625q0 -0.765625 -0.390625 -1.328125q-0.390625 -0.5625 -1.078125 -0.78125q-0.6875 -0.234375 -1.984375 -0.234375l-3.15625 0l0 4.671875zm16.959198 1.609375l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm3.9382324 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.097931 0l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.566711 0l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375z" fill-rule="nonzero"></path><path fill="#efefef" d="m410.65585 106.94125l147.21262 0l0 59.74803l-147.21262 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m410.65585 106.94125l147.21262 0l0 59.74803l-147.21262 0z" fill-rule="nonzero"></path><path fill="#000000" d="m457.33118 130.14151l1.796875 0l0 7.84375q0 2.0625 -0.46875 3.265625q-0.453125 1.203125 -1.671875 1.96875q-1.203125 0.75 -3.171875 0.75q-1.90625 0 -3.125 -0.65625q-1.21875 -0.65625 -1.734375 -1.90625q-0.515625 -1.25 -0.515625 -3.421875l0 -7.84375l1.796875 0l0 7.84375q0 1.765625 0.328125 2.609375q0.328125 0.84375 1.125 1.296875q0.8125 0.453125 1.96875 0.453125q1.984375 0 2.828125 -0.890625q0.84375 -0.90625 0.84375 -3.46875l0 -7.84375zm4.332306 13.59375l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm10.391357 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm7.785431 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm10.382233 1.5l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438202 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m596.24115 127.19322l147.21259 0l0 59.74803l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m596.24115 127.19322l147.21259 0l0 59.74803l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m627.5322 152.98723l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.6676636 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031982 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277039 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813232 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6657715 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351074 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m656.62775 174.98723l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438232 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m586.65326 117.082985l147.21259 0l0 59.748024l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m586.65326 117.082985l147.21259 0l0 59.748024l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m617.9443 142.877l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.6677246 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031921 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.2771 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813171 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6658325 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7350464 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5427246 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5041504 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m647.03986 164.877l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438171 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#efefef" d="m577.0548 106.94125l147.21259 0l0 59.74803l-147.21259 0z" fill-rule="nonzero"></path><path stroke="#999999" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m577.0548 106.94125l147.21259 0l0 59.74803l-147.21259 0z" fill-rule="nonzero"></path><path fill="#000000" d="m608.3459 132.73526l0 -13.593742l1.8125 0l0 13.593742l-1.8125 0zm4.6676636 0l0 -9.859367l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0624924l-1.671875 0l0 -5.9999924q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.3749924l-1.671875 0zm14.031982 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.6562424l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.7499924q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277039 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.7343674q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.43749237l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.7031174l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813232 6.6874924l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.5156174q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515617q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.9218674q0 1.9531174 0.765625 2.8437424q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.7812424q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.1093674l0 -9.859367l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.1562424l-1.671875 0zm12.6657715 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.9531174q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.2343674q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.7187424q-0.90625 0.35936737 -2.734375 0.6249924q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.6093674zm7.7351074 3.4374924l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.6562424l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.7499924q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.187492l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.687492l0 -9.859367l1.671875 0l0 9.859367l-1.671875 0zm3.5042114 -4.9218674q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.9062424 -0.578125 2.9999924q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8124924zm1.71875 0q0 1.8906174 0.828125 2.8281174q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.8906174q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.9218674l0 -9.859367l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0624924l-1.671875 0l0 -5.9999924q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.3749924l-1.671875 0z" fill-rule="nonzero"></path><path fill="#000000" d="m637.4414 154.73526l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438232 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m391.4695 136.81526l9.593231 0l0 0.06300354l9.58786 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m391.46954 136.81526l9.593201 0l0 0.06300354l3.5878906 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m404.65063 138.53l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m557.86847 136.81526l9.593201 0l0 0.06300354l9.587891 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m557.86847 136.81526l9.593201 0l0 0.06300354l3.5878906 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m571.04956 138.53l4.538147 -1.6517334l-4.538147 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m724.2674 136.81526l20.531738 0l0 0.06300354l20.539124 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m724.2674 136.81526l20.531738 0l0 0.06300354l14.539124 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m759.33826 138.53l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path></g></svg>
+
diff --git a/doc/ci/img/types-of-pipelines.svg b/doc/ci/img/types-of-pipelines.svg
new file mode 100644
index 00000000000..b63b5f56ba6
--- /dev/null
+++ b/doc/ci/img/types-of-pipelines.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" standalone="yes"?>
+
+<svg version="1.1" viewBox="0.0 0.0 740.6272965879265 293.7795275590551" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="p.0"><path d="m0 0l740.6273 0l0 293.77954l-740.6273 0l0 -293.77954z" clip-rule="nonzero"></path></clipPath><g clip-path="url(#p.0)"><path fill="#000000" fill-opacity="0.0" d="m0 0l740.6273 0l0 293.77954l-740.6273 0z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m176.05511 4.632546l282.4567 0l0 129.10236l-282.4567 0z" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="4.0,3.0" d="m176.05511 4.632546l282.4567 0l0 129.10236l-282.4567 0z" fill-rule="nonzero"></path><path fill="#ff0000" d="m283.34512 115.88928l1.796875 0.453125q-0.5625 2.21875 -2.03125 3.390625q-1.46875 1.15625 -3.59375 1.15625q-2.203125 0 -3.578125 -0.890625q-1.375 -0.90625 -2.09375 -2.59375q-0.71875 -1.703125 -0.71875 -3.65625q0 -2.125 0.796875 -3.703125q0.8125 -1.578125 2.3125 -2.390625q1.5 -0.828125 3.296875 -0.828125q2.046875 0 3.4375 1.046875q1.390625 1.03125 1.9375 2.90625l-1.765625 0.421875q-0.46875 -1.484375 -1.375 -2.15625q-0.90625 -0.6875 -2.265625 -0.6875q-1.5625 0 -2.625 0.75q-1.046875 0.75 -1.484375 2.03125q-0.421875 1.265625 -0.421875 2.609375q0 1.734375 0.5 3.03125q0.515625 1.28125 1.578125 1.921875q1.078125 0.640625 2.3125 0.640625q1.515625 0 2.5625 -0.859375q1.046875 -0.875 1.421875 -2.59375zm4.066681 4.765625l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm10.069733 0l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.443573 -4.546875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 3.78125l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm15.610077 1.703125l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078857 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm4.191681 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm17.125702 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m482.78796 22.986877l110.11023 0l0 59.74803l-110.11023 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m482.78796 22.986877l110.11023 0l0 59.74803l-110.11023 0z" fill-rule="nonzero"></path><path fill="#434343" d="m499.90414 48.78089l0 -13.59375l4.6875 0q1.578125 0 2.421875 0.1875q1.15625 0.265625 1.984375 0.96875q1.078125 0.921875 1.609375 2.34375q0.53125 1.40625 0.53125 3.21875q0 1.546875 -0.359375 2.75q-0.359375 1.1875 -0.921875 1.984375q-0.5625 0.78125 -1.234375 1.234375q-0.671875 0.4375 -1.625 0.671875q-0.953125 0.234375 -2.1875 0.234375l-4.90625 0zm1.796875 -1.609375l2.90625 0q1.34375 0 2.109375 -0.25q0.765625 -0.25 1.21875 -0.703125q0.640625 -0.640625 1.0 -1.71875q0.359375 -1.078125 0.359375 -2.625q0 -2.125 -0.703125 -3.265625q-0.703125 -1.15625 -1.703125 -1.546875q-0.71875 -0.28125 -2.328125 -0.28125l-2.859375 0l0 10.390625zm18.207306 -1.5625l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.110107 9.65625l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm8.828857 4.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm3.5510864 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.203796 8.71875l-0.171875 -1.5625q0.546875 0.140625 0.953125 0.140625q0.546875 0 0.875 -0.1875q0.34375 -0.1875 0.5625 -0.515625q0.15625 -0.25 0.5 -1.25q0.046875 -0.140625 0.15625 -0.40625l-3.734375 -9.875l1.796875 0l2.046875 5.71875q0.40625 1.078125 0.71875 2.28125q0.28125 -1.15625 0.6875 -2.25l2.09375 -5.75l1.671875 0l-3.75 10.03125q-0.59375 1.625 -0.9375 2.234375q-0.4375 0.828125 -1.015625 1.203125q-0.578125 0.390625 -1.375 0.390625q-0.484375 0 -1.078125 -0.203125zm18.245789 -5.296875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.9020996 -3.421875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125z" fill-rule="nonzero"></path><path fill="#434343" d="m507.0652 66.40589l1.6875 -0.140625q0.125 1.015625 0.5625 1.671875q0.4375 0.65625 1.359375 1.0625q0.9375 0.40625 2.09375 0.40625q1.03125 0 1.8125 -0.3125q0.796875 -0.3125 1.1875 -0.84375q0.390625 -0.53125 0.390625 -1.15625q0 -0.640625 -0.375 -1.109375q-0.375 -0.484375 -1.234375 -0.8125q-0.546875 -0.21875 -2.421875 -0.65625q-1.875 -0.453125 -2.625 -0.859375q-0.96875 -0.515625 -1.453125 -1.265625q-0.46875 -0.75 -0.46875 -1.6875q0 -1.03125 0.578125 -1.921875q0.59375 -0.90625 1.703125 -1.359375q1.125 -0.46875 2.5 -0.46875q1.515625 0 2.671875 0.484375q1.15625 0.484375 1.765625 1.4375q0.625 0.9375 0.671875 2.140625l-1.71875 0.125q-0.140625 -1.28125 -0.953125 -1.9375q-0.796875 -0.671875 -2.359375 -0.671875q-1.625 0 -2.375 0.609375q-0.75 0.59375 -0.75 1.4375q0 0.734375 0.53125 1.203125q0.515625 0.46875 2.703125 0.96875q2.203125 0.5 3.015625 0.875q1.1875 0.546875 1.75 1.390625q0.578125 0.828125 0.578125 1.921875q0 1.09375 -0.625 2.0625q-0.625 0.953125 -1.796875 1.484375q-1.15625 0.53125 -2.609375 0.53125q-1.84375 0 -3.09375 -0.53125q-1.25 -0.546875 -1.96875 -1.625q-0.703125 -1.078125 -0.734375 -2.453125zm16.490417 2.875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm7.9645996 0.28125q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm3.7819214 5.75l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.313232 -6.578125l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm10.078796 0.8125l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m606.1778 23.019379l110.11023 0l0 59.74803l-110.11023 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m606.1778 23.019379l110.11023 0l0 59.74803l-110.11023 0z" fill-rule="nonzero"></path><path fill="#434343" d="m623.29395 48.813393l0 -13.59375l4.6875 0q1.578125 0 2.421875 0.1875q1.15625 0.265625 1.984375 0.96875q1.078125 0.921875 1.609375 2.34375q0.53125 1.40625 0.53125 3.21875q0 1.546875 -0.359375 2.75q-0.359375 1.1875 -0.921875 1.984375q-0.5625 0.78125 -1.234375 1.234375q-0.671875 0.4375 -1.625 0.671875q-0.953125 0.234375 -2.1875 0.234375l-4.90625 0zm1.796875 -1.609375l2.90625 0q1.34375 0 2.109375 -0.25q0.765625 -0.25 1.21875 -0.703125q0.640625 -0.640625 1.0 -1.71875q0.359375 -1.078125 0.359375 -2.625q0 -2.125 -0.703125 -3.265625q-0.703125 -1.15625 -1.703125 -1.546875q-0.71875 -0.28125 -2.328125 -0.28125l-2.859375 0l0 10.390625zm18.207336 -1.5625l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.110107 9.65625l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm8.828857 4.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm3.5510254 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.203857 8.71875l-0.171875 -1.5625q0.546875 0.140625 0.953125 0.140625q0.546875 0 0.875 -0.1875q0.34375 -0.1875 0.5625 -0.515625q0.15625 -0.25 0.5 -1.25q0.046875 -0.140625 0.15625 -0.40625l-3.734375 -9.875l1.796875 0l2.046875 5.71875q0.40625 1.078125 0.71875 2.28125q0.28125 -1.15625 0.6875 -2.25l2.09375 -5.75l1.671875 0l-3.75 10.03125q-0.59375 1.625 -0.9375 2.234375q-0.4375 0.828125 -1.015625 1.203125q-0.578125 0.390625 -1.375 0.390625q-0.484375 0 -1.078125 -0.203125zm18.245789 -5.296875l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.9020996 -3.421875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125z" fill-rule="nonzero"></path><path fill="#434343" d="m618.10614 70.81339l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.4122925 7.140625l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.6033325 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672546 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm15.719482 4.921875l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm10.360107 -3.609375l1.640625 0.21875q-0.265625 1.6875 -1.375 2.65625q-1.109375 0.953125 -2.734375 0.953125q-2.015625 0 -3.25 -1.3125q-1.21875 -1.328125 -1.21875 -3.796875q0 -1.59375 0.515625 -2.78125q0.53125 -1.203125 1.609375 -1.796875q1.09375 -0.609375 2.359375 -0.609375q1.609375 0 2.625 0.8125q1.015625 0.8125 1.3125 2.3125l-1.625 0.25q-0.234375 -1.0 -0.828125 -1.5q-0.59375 -0.5 -1.421875 -0.5q-1.265625 0 -2.0625 0.90625q-0.78125 0.90625 -0.78125 2.859375q0 1.984375 0.765625 2.890625q0.765625 0.890625 1.984375 0.890625q0.984375 0 1.640625 -0.59375q0.65625 -0.609375 0.84375 -1.859375zm6.546875 2.109375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m192.52992 22.986877l110.07875 0l0 59.74803l-110.07875 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m192.52992 22.986877l110.07875 0l0 59.74803l-110.07875 0z" fill-rule="nonzero"></path><path fill="#434343" d="m228.21759 59.78089l0 -13.59375l5.109375 0q1.546875 0 2.484375 0.40625q0.953125 0.40625 1.484375 1.265625q0.53125 0.859375 0.53125 1.796875q0 0.875 -0.46875 1.65625q-0.46875 0.765625 -1.4375 1.234375q1.234375 0.359375 1.890625 1.234375q0.671875 0.875 0.671875 2.0625q0 0.953125 -0.40625 1.78125q-0.390625 0.8125 -0.984375 1.265625q-0.59375 0.4375 -1.5 0.671875q-0.890625 0.21875 -2.1875 0.21875l-5.1875 0zm1.796875 -7.890625l2.9375 0q1.203125 0 1.71875 -0.15625q0.6875 -0.203125 1.03125 -0.671875q0.359375 -0.46875 0.359375 -1.1875q0 -0.671875 -0.328125 -1.1875q-0.328125 -0.515625 -0.9375 -0.703125q-0.59375 -0.203125 -2.0625 -0.203125l-2.71875 0l0 4.109375zm0 6.28125l3.390625 0q0.875 0 1.21875 -0.0625q0.625 -0.109375 1.046875 -0.359375q0.421875 -0.265625 0.6875 -0.765625q0.265625 -0.5 0.265625 -1.140625q0 -0.765625 -0.390625 -1.328125q-0.390625 -0.5625 -1.078125 -0.78125q-0.6875 -0.234375 -1.984375 -0.234375l-3.15625 0l0 4.671875zm16.959198 1.609375l0 -1.453125q-1.140625 1.671875 -3.125 1.671875q-0.859375 0 -1.625 -0.328125q-0.75 -0.34375 -1.125 -0.84375q-0.359375 -0.5 -0.515625 -1.234375q-0.09375 -0.5 -0.09375 -1.5625l0 -6.109375l1.671875 0l0 5.46875q0 1.3125 0.09375 1.765625q0.15625 0.65625 0.671875 1.03125q0.515625 0.375 1.265625 0.375q0.75 0 1.40625 -0.375q0.65625 -0.390625 0.921875 -1.046875q0.28125 -0.671875 0.28125 -1.9375l0 -5.28125l1.671875 0l0 9.859375l-1.5 0zm3.9382172 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.097946 0l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.566681 0l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m331.2466 43.238846l110.078735 0l0 59.74803l-110.078735 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m331.2466 43.238846l110.078735 0l0 59.74803l-110.078735 0z" fill-rule="nonzero"></path><path fill="#434343" d="m343.97073 69.03286l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.667694 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031952 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277069 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813202 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.665802 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.735077 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426941 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.504181 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281982 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#434343" d="m373.06628 91.03286l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438202 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m324.0777 33.12861l110.078766 0l0 59.74803l-110.078766 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m324.0777 33.12861l110.078766 0l0 59.74803l-110.078766 0z" fill-rule="nonzero"></path><path fill="#434343" d="m336.80185 58.922623l0 -13.59375l1.8125 0l0 13.59375l-1.8125 0zm4.667694 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm14.031952 -1.5l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277069 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.813202 6.6875l1.609375 0.25q0.109375 0.75 0.578125 1.09375q0.609375 0.453125 1.6875 0.453125q1.171875 0 1.796875 -0.46875q0.625 -0.453125 0.859375 -1.28125q0.125 -0.515625 0.109375 -2.15625q-1.09375 1.296875 -2.71875 1.296875q-2.03125 0 -3.15625 -1.46875q-1.109375 -1.46875 -1.109375 -3.515625q0 -1.40625 0.515625 -2.59375q0.515625 -1.203125 1.484375 -1.84375q0.96875 -0.65625 2.265625 -0.65625q1.75 0 2.875 1.40625l0 -1.1875l1.546875 0l0 8.515625q0 2.3125 -0.46875 3.265625q-0.46875 0.96875 -1.484375 1.515625q-1.015625 0.5625 -2.5 0.5625q-1.765625 0 -2.859375 -0.796875q-1.078125 -0.796875 -1.03125 -2.390625zm1.375 -5.921875q0 1.953125 0.765625 2.84375q0.78125 0.890625 1.9375 0.890625q1.140625 0 1.921875 -0.890625q0.78125 -0.890625 0.78125 -2.78125q0 -1.8125 -0.8125 -2.71875q-0.796875 -0.921875 -1.921875 -0.921875q-1.109375 0 -1.890625 0.90625q-0.78125 0.890625 -0.78125 2.671875zm9.281982 5.109375l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm12.6657715 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.8593445 0.3125 -1.8437195 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.0312195 -0.25 2.9843445 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.9062195 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.2499695 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.7343445 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.9687195 0 1.7187195 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351074 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm1.5426636 -10.1875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm3.5042114 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281952 4.921875l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0z" fill-rule="nonzero"></path><path fill="#434343" d="m365.8974 80.92262l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474091 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438232 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125z" fill-rule="nonzero"></path><path fill="#f3f3f3" d="m316.90097 22.986877l110.078735 0l0 59.74803l-110.078735 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m316.90097 22.986877l110.078735 0l0 59.74803l-110.078735 0z" fill-rule="nonzero"></path><path fill="#434343" d="m354.05658 59.78089l0 -12.0l-4.46875 0l0 -1.59375l10.765625 0l0 1.59375l-4.5 0l0 12.0l-1.796875 0zm14.474121 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm8.438202 2.9375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375zm13.65625 1.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm0.8551941 -1.4375l1.65625 -0.265625q0.140625 1.0 0.765625 1.53125q0.640625 0.515625 1.78125 0.515625q1.15625 0 1.703125 -0.46875q0.5625 -0.46875 0.5625 -1.09375q0 -0.5625 -0.484375 -0.890625q-0.34375 -0.21875 -1.703125 -0.5625q-1.84375 -0.46875 -2.5625 -0.796875q-0.703125 -0.34375 -1.078125 -0.9375q-0.359375 -0.609375 -0.359375 -1.328125q0 -0.65625 0.296875 -1.21875q0.3125 -0.5625 0.828125 -0.9375q0.390625 -0.28125 1.0625 -0.484375q0.671875 -0.203125 1.4375 -0.203125q1.171875 0 2.046875 0.34375q0.875 0.328125 1.28125 0.90625q0.421875 0.5625 0.578125 1.515625l-1.625 0.21875q-0.109375 -0.75 -0.65625 -1.171875q-0.53125 -0.4375 -1.5 -0.4375q-1.15625 0 -1.640625 0.390625q-0.484375 0.375 -0.484375 0.875q0 0.328125 0.203125 0.59375q0.203125 0.265625 0.640625 0.4375q0.25 0.09375 1.46875 0.4375q1.765625 0.46875 2.46875 0.765625q0.703125 0.296875 1.09375 0.875q0.40625 0.578125 0.40625 1.4375q0 0.828125 -0.484375 1.578125q-0.484375 0.734375 -1.40625 1.140625q-0.921875 0.390625 -2.078125 0.390625q-1.921875 0 -2.9375 -0.796875q-1.0 -0.796875 -1.28125 -2.359375z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m302.60867 52.860893l7.165344 0l0 0.062992096l7.165344 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m302.60864 52.860893l7.1653748 0l0 0.062992096l1.1653442 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m310.93936 54.57562l4.5381165 -1.6517334l-4.5381165 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m426.9797 52.860893l27.904388 0l0 0.062992096l27.906647 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m426.9797 52.860893l27.904388 0l0 0.062992096l21.906616 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m476.7907 54.57562l4.5381165 -1.6517334l-4.5381165 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m592.8982 52.860893l6.6398315 0l0 0.062992096l6.6514893 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m592.8982 52.860893l6.6398315 0l0 0.062992096l0.6515503 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m600.1896 54.57562l4.538086 -1.6517334l-4.538086 -1.6517334z" fill-rule="evenodd"></path><path fill="#f3f3f3" d="m26.104986 22.986877l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m26.104986 22.986877l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path fill="#434343" d="m70.03193 55.015266l1.796875 0.453125q-0.5625 2.21875 -2.03125 3.390625q-1.46875 1.15625 -3.59375 1.15625q-2.203125 0 -3.5781212 -0.890625q-1.375 -0.90625 -2.09375 -2.59375q-0.71875 -1.703125 -0.71875 -3.65625q0 -2.125 0.796875 -3.703125q0.8125 -1.578125 2.3125 -2.390625q1.4999962 -0.828125 3.2968712 -0.828125q2.046875 0 3.4375 1.046875q1.390625 1.03125 1.9375 2.90625l-1.765625 0.421875q-0.46875 -1.484375 -1.375 -2.15625q-0.90625 -0.6875 -2.265625 -0.6875q-1.5625 0 -2.6249962 0.75q-1.046875 0.75 -1.484375 2.03125q-0.421875 1.265625 -0.421875 2.609375q0 1.734375 0.5 3.03125q0.515625 1.28125 1.578125 1.921875q1.0781212 0.640625 2.3124962 0.640625q1.515625 0 2.5625 -0.859375q1.046875 -0.875 1.421875 -2.59375zm2.9260712 -0.15625q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672592 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm16.016342 1.75l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m136.21523 52.860893l28.15747 0l0 0.062992096l28.157486 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m136.21523 52.860893l28.15747 0l0 0.062992096l22.157486 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m186.53018 54.57562l4.538101 -1.6517334l-4.538101 -1.6517334z" fill-rule="evenodd"></path><path fill="#f3f3f3" d="m26.104986 120.98688l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m26.104986 120.98688l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path fill="#434343" d="m50.508137 146.78088l0 -13.59375l6.03125 0q1.8125 0 2.75 0.359375q0.953125 0.359375 1.515625 1.296875q0.5625 0.921875 0.5625 2.046875q0 1.453125 -0.9375 2.453125q-0.921875 0.984375 -2.890625 1.25q0.71875 0.34375 1.09375 0.671875q0.78125 0.734375 1.484375 1.8125l2.375 3.703125l-2.265625 0l-1.796875 -2.828125q-0.796875 -1.21875 -1.3125 -1.875q-0.5 -0.65625 -0.90625 -0.90625q-0.40625 -0.265625 -0.8125 -0.359375q-0.3125 -0.078125 -1.015625 -0.078125l-2.078125 0l0 6.046875l-1.796875 0zm1.796875 -7.59375l3.859375 0q1.234375 0 1.921875 -0.25q0.703125 -0.265625 1.0625 -0.828125q0.375 -0.5625 0.375 -1.21875q0 -0.96875 -0.703125 -1.578125q-0.703125 -0.625 -2.21875 -0.625l-4.296875 0l0 4.5zm18.176067 4.421875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.2656212 -1.328125 -1.2656212 -3.734375q0 -2.484375 1.2656212 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078842 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.613571 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.4062576 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.6562576 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277054 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm15.500717 5.875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375z" fill-rule="nonzero"></path><path fill="#434343" d="m70.03193 164.01526l1.796875 0.453125q-0.5625 2.21875 -2.03125 3.390625q-1.46875 1.15625 -3.59375 1.15625q-2.203125 0 -3.5781212 -0.890625q-1.375 -0.90625 -2.09375 -2.59375q-0.71875 -1.703125 -0.71875 -3.65625q0 -2.125 0.796875 -3.703125q0.8125 -1.578125 2.3125 -2.390625q1.4999962 -0.828125 3.2968712 -0.828125q2.046875 0 3.4375 1.046875q1.390625 1.03125 1.9375 2.90625l-1.765625 0.421875q-0.46875 -1.484375 -1.375 -2.15625q-0.90625 -0.6875 -2.265625 -0.6875q-1.5625 0 -2.6249962 0.75q-1.046875 0.75 -1.484375 2.03125q-0.421875 1.265625 -0.421875 2.609375q0 1.734375 0.5 3.03125q0.515625 1.28125 1.578125 1.921875q1.0781212 0.640625 2.3124962 0.640625q1.515625 0 2.5625 -0.859375q1.046875 -0.875 1.421875 -2.59375zm2.9260712 -0.15625q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672592 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm16.016342 1.75l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m136.21523 150.86089l28.15747 0l0 -98.01574l28.157486 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m136.21523 150.86089l28.15747 0l0 -98.01574l22.157486 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m186.53018 54.496876l4.538101 -1.6517296l-4.538101 -1.6517334z" fill-rule="evenodd"></path><path fill="#f3f3f3" d="m26.104986 190.98688l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path stroke="#b7b7b7" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m26.104986 190.98688l110.11024 0l0 59.74803l-110.11024 0z" fill-rule="nonzero"></path><path fill="#434343" d="m50.508137 216.78088l0 -13.59375l6.03125 0q1.8125 0 2.75 0.359375q0.953125 0.359375 1.515625 1.296875q0.5625 0.921875 0.5625 2.046875q0 1.453125 -0.9375 2.453125q-0.921875 0.984375 -2.890625 1.25q0.71875 0.34375 1.09375 0.671875q0.78125 0.734375 1.484375 1.8125l2.375 3.703125l-2.265625 0l-1.796875 -2.828125q-0.796875 -1.21875 -1.3125 -1.875q-0.5 -0.65625 -0.90625 -0.90625q-0.40625 -0.265625 -0.8125 -0.359375q-0.3125 -0.078125 -1.015625 -0.078125l-2.078125 0l0 6.046875l-1.796875 0zm1.796875 -7.59375l3.859375 0q1.234375 0 1.921875 -0.25q0.703125 -0.265625 1.0625 -0.828125q0.375 -0.5625 0.375 -1.21875q0 -0.96875 -0.703125 -1.578125q-0.703125 -0.625 -2.21875 -0.625l-4.296875 0l0 4.5zm18.176067 4.421875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.2656212 -1.328125 -1.2656212 -3.734375q0 -2.484375 1.2656212 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078842 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm10.613571 -1.21875q-0.9375 0.796875 -1.796875 1.125q-0.859375 0.3125 -1.84375 0.3125q-1.609375 0 -2.484375 -0.78125q-0.875 -0.796875 -0.875 -2.03125q0 -0.734375 0.328125 -1.328125q0.328125 -0.59375 0.859375 -0.953125q0.53125 -0.359375 1.203125 -0.546875q0.5 -0.140625 1.484375 -0.25q2.03125 -0.25 2.984375 -0.578125q0 -0.34375 0 -0.4375q0 -1.015625 -0.46875 -1.4375q-0.640625 -0.5625 -1.90625 -0.5625q-1.171875 0 -1.734375 0.40625q-0.5625 0.40625 -0.828125 1.46875l-1.640625 -0.234375q0.234375 -1.046875 0.734375 -1.6875q0.515625 -0.640625 1.46875 -0.984375q0.96875 -0.359375 2.25 -0.359375q1.265625 0 2.046875 0.296875q0.78125 0.296875 1.15625 0.75q0.375 0.453125 0.515625 1.140625q0.09375 0.421875 0.09375 1.53125l0 2.234375q0 2.328125 0.09375 2.953125q0.109375 0.609375 0.4375 1.171875l-1.75 0q-0.265625 -0.515625 -0.328125 -1.21875zm-0.140625 -3.71875q-0.90625 0.359375 -2.734375 0.625q-1.03125 0.140625 -1.453125 0.328125q-0.421875 0.1875 -0.65625 0.546875q-0.234375 0.359375 -0.234375 0.796875q0 0.671875 0.5 1.125q0.515625 0.4375 1.484375 0.4375q0.96875 0 1.71875 -0.421875q0.75 -0.4375 1.109375 -1.15625q0.265625 -0.578125 0.265625 -1.671875l0 -0.609375zm7.7351 3.4375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.4062576 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.6562576 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm8.277054 -1.671875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm15.500717 5.875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375z" fill-rule="nonzero"></path><path fill="#434343" d="m70.03193 234.01526l1.796875 0.453125q-0.5625 2.21875 -2.03125 3.390625q-1.46875 1.15625 -3.59375 1.15625q-2.203125 0 -3.5781212 -0.890625q-1.375 -0.90625 -2.09375 -2.59375q-0.71875 -1.703125 -0.71875 -3.65625q0 -2.125 0.796875 -3.703125q0.8125 -1.578125 2.3125 -2.390625q1.4999962 -0.828125 3.2968712 -0.828125q2.046875 0 3.4375 1.046875q1.390625 1.03125 1.9375 2.90625l-1.765625 0.421875q-0.46875 -1.484375 -1.375 -2.15625q-0.90625 -0.6875 -2.265625 -0.6875q-1.5625 0 -2.6249962 0.75q-1.046875 0.75 -1.484375 2.03125q-0.421875 1.265625 -0.421875 2.609375q0 1.734375 0.5 3.03125q0.515625 1.28125 1.578125 1.921875q1.0781212 0.640625 2.3124962 0.640625q1.515625 0 2.5625 -0.859375q1.046875 -0.875 1.421875 -2.59375zm2.9260712 -0.15625q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm15.672592 4.921875l0 -1.25q-0.9375 1.46875 -2.75 1.46875q-1.171875 0 -2.171875 -0.640625q-0.984375 -0.65625 -1.53125 -1.8125q-0.53125 -1.171875 -0.53125 -2.6875q0 -1.46875 0.484375 -2.671875q0.5 -1.203125 1.46875 -1.84375q0.984375 -0.640625 2.203125 -0.640625q0.890625 0 1.578125 0.375q0.703125 0.375 1.140625 0.984375l0 -4.875l1.65625 0l0 13.59375l-1.546875 0zm-5.28125 -4.921875q0 1.890625 0.796875 2.828125q0.8125 0.9375 1.890625 0.9375q1.09375 0 1.859375 -0.890625q0.765625 -0.890625 0.765625 -2.734375q0 -2.015625 -0.78125 -2.953125q-0.78125 -0.953125 -1.921875 -0.953125q-1.109375 0 -1.859375 0.90625q-0.75 0.90625 -0.75 2.859375zm16.016342 1.75l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m136.21523 220.86089l28.15747 0l0 -168.0l28.157486 0" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m136.21523 220.86089l28.15747 0l0 -168.0l22.157486 0" fill-rule="evenodd"></path><path fill="#000000" stroke="#000000" stroke-width="1.0" stroke-linecap="butt" d="m186.53018 54.512627l4.538101 -1.6517334l-4.538101 -1.6517334z" fill-rule="evenodd"></path><path fill="#000000" fill-opacity="0.0" d="m5.1522307 4.632546l165.85826 0l0 283.52756l-165.85826 0z" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="4.0,3.0" d="m5.1522307 4.632546l165.85826 0l0 283.52756l-165.85826 0z" fill-rule="nonzero"></path><path fill="#ff0000" d="m24.73604 275.0801l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q0.9999981 0.171875 1.6718731 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984373 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.468748 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.1249981 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.412321 7.140625l0 -9.859375l1.5 0l0 1.5q0.578125 -1.046875 1.0625 -1.375q0.484375 -0.34375 1.078125 -0.34375q0.84375 0 1.71875 0.546875l-0.578125 1.546875q-0.609375 -0.359375 -1.234375 -0.359375q-0.546875 0 -0.984375 0.328125q-0.421875 0.328125 -0.609375 0.90625q-0.28125 0.890625 -0.28125 1.953125l0 5.15625l-1.671875 0zm5.603302 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.281967 -6.734375l0 -1.9375l1.65625 0l0 1.9375l-1.65625 0zm-2.125 15.484375l0.3125 -1.421875q0.5 0.125 0.796875 0.125q0.515625 0 0.765625 -0.34375q0.25 -0.328125 0.25 -1.6875l0 -10.359375l1.65625 0l0 10.390625q0 1.828125 -0.46875 2.546875q-0.59375 0.921875 -2.0 0.921875q-0.671875 0 -1.3125 -0.171875zm13.019821 -7.0l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm15.547592 2.265625l1.640625 0.21875q-0.265625 1.6875 -1.375 2.65625q-1.109375 0.953125 -2.734375 0.953125q-2.015625 0 -3.25 -1.3125q-1.21875 -1.328125 -1.21875 -3.796875q0 -1.59375 0.515625 -2.78125q0.53125 -1.203125 1.609375 -1.796875q1.09375 -0.609375 2.359375 -0.609375q1.609375 0 2.625 0.8125q1.015625 0.8125 1.3125 2.3125l-1.625 0.25q-0.234375 -1.0 -0.828125 -1.5q-0.59375 -0.5 -1.421875 -0.5q-1.265625 0 -2.0625 0.90625q-0.78125 0.90625 -0.78125 2.859375q0 1.984375 0.765625 2.890625q0.765625 0.890625 1.984375 0.890625q0.984375 0 1.640625 -0.59375q0.65625 -0.609375 0.84375 -1.859375zm6.546875 2.109375l0.234375 1.484375q-0.703125 0.140625 -1.265625 0.140625q-0.90625 0 -1.40625 -0.28125q-0.5 -0.296875 -0.703125 -0.75q-0.203125 -0.46875 -0.203125 -1.984375l0 -5.65625l-1.234375 0l0 -1.3125l1.234375 0l0 -2.4375l1.65625 -1.0l0 3.4375l1.6875 0l0 1.3125l-1.6875 0l0 5.75q0 0.71875 0.078125 0.921875q0.09375 0.203125 0.296875 0.328125q0.203125 0.125 0.578125 0.125q0.265625 0 0.734375 -0.078125zm6.9291077 1.5l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.443573 -4.546875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.129196 3.78125l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm15.610092 1.703125l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078842 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm4.191696 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.129196 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm17.125732 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path><path fill="#000000" fill-opacity="0.0" d="m464.70865 4.632546l270.23624 0l0 129.10236l-270.23624 0z" fill-rule="nonzero"></path><path stroke="#000000" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="4.0,3.0" d="m464.70865 4.632546l270.23624 0l0 129.10236l-270.23624 0z" fill-rule="nonzero"></path><path fill="#ff0000" d="m536.47687 120.65491l0 -13.59375l4.6875 0q1.578125 0 2.421875 0.1875q1.15625 0.265625 1.984375 0.96875q1.078125 0.921875 1.609375 2.34375q0.53125 1.40625 0.53125 3.21875q0 1.546875 -0.359375 2.75q-0.359375 1.1875 -0.921875 1.984375q-0.5625 0.78125 -1.234375 1.234375q-0.671875 0.4375 -1.625 0.671875q-0.953125 0.234375 -2.1875 0.234375l-4.90625 0zm1.796875 -1.609375l2.90625 0q1.34375 0 2.109375 -0.25q0.765625 -0.25 1.21875 -0.703125q0.640625 -0.640625 1.0 -1.71875q0.359375 -1.078125 0.359375 -2.625q0 -2.125 -0.703125 -3.265625q-0.703125 -1.15625 -1.703125 -1.546875q-0.71875 -0.28125 -2.328125 -0.28125l-2.859375 0l0 10.390625zm18.207336 -1.5625l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.110107 9.65625l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm8.828857 4.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm3.5510254 -4.921875q0 -2.734375 1.53125 -4.0625q1.265625 -1.09375 3.09375 -1.09375q2.03125 0 3.3125 1.34375q1.296875 1.328125 1.296875 3.671875q0 1.90625 -0.578125 3.0q-0.5625 1.078125 -1.65625 1.6875q-1.078125 0.59375 -2.375 0.59375q-2.0625 0 -3.34375 -1.328125q-1.28125 -1.328125 -1.28125 -3.8125zm1.71875 0q0 1.890625 0.828125 2.828125q0.828125 0.9375 2.078125 0.9375q1.25 0 2.0625 -0.9375q0.828125 -0.953125 0.828125 -2.890625q0 -1.828125 -0.828125 -2.765625q-0.828125 -0.9375 -2.0625 -0.9375q-1.25 0 -2.078125 0.9375q-0.828125 0.9375 -0.828125 2.828125zm9.203857 8.71875l-0.171875 -1.5625q0.546875 0.140625 0.953125 0.140625q0.546875 0 0.875 -0.1875q0.34375 -0.1875 0.5625 -0.515625q0.15625 -0.25 0.5 -1.25q0.046875 -0.140625 0.15625 -0.40625l-3.734375 -9.875l1.796875 0l2.046875 5.71875q0.40625 1.078125 0.71875 2.28125q0.28125 -1.15625 0.6875 -2.25l2.09375 -5.75l1.671875 0l-3.75 10.03125q-0.59375 1.625 -0.9375 2.234375q-0.4375 0.828125 -1.015625 1.203125q-0.578125 0.390625 -1.375 0.390625q-0.484375 0 -1.078125 -0.203125zm14.808289 -3.796875l0 -13.59375l5.125 0q1.359375 0 2.078125 0.125q1.0 0.171875 1.671875 0.640625q0.671875 0.46875 1.078125 1.3125q0.421875 0.84375 0.421875 1.84375q0 1.734375 -1.109375 2.9375q-1.09375 1.203125 -3.984375 1.203125l-3.484375 0l0 5.53125l-1.796875 0zm1.796875 -7.140625l3.515625 0q1.75 0 2.46875 -0.640625q0.734375 -0.65625 0.734375 -1.828125q0 -0.859375 -0.4375 -1.46875q-0.421875 -0.609375 -1.125 -0.796875q-0.453125 -0.125 -1.671875 -0.125l-3.484375 0l0 4.859375zm10.4436035 -4.546875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 3.78125l0 -13.640625l1.53125 0l0 1.28125q0.53125 -0.75 1.203125 -1.125q0.6875 -0.375 1.640625 -0.375q1.265625 0 2.234375 0.65625q0.96875 0.640625 1.453125 1.828125q0.5 1.1875 0.5 2.59375q0 1.515625 -0.546875 2.734375q-0.546875 1.203125 -1.578125 1.84375q-1.03125 0.640625 -2.171875 0.640625q-0.84375 0 -1.515625 -0.34375q-0.65625 -0.359375 -1.078125 -0.890625l0 4.796875l-1.671875 0zm1.515625 -8.65625q0 1.90625 0.765625 2.8125q0.78125 0.90625 1.875 0.90625q1.109375 0 1.890625 -0.9375q0.796875 -0.9375 0.796875 -2.921875q0 -1.875 -0.78125 -2.8125q-0.765625 -0.9375 -1.84375 -0.9375q-1.0625 0 -1.890625 1.0q-0.8125 1.0 -0.8125 2.890625zm15.610046 1.703125l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875zm9.078857 5.875l0 -13.59375l1.671875 0l0 13.59375l-1.671875 0zm4.1917114 -11.6875l0 -1.90625l1.671875 0l0 1.90625l-1.671875 0zm0 11.6875l0 -9.859375l1.671875 0l0 9.859375l-1.671875 0zm4.1292114 0l0 -9.859375l1.5 0l0 1.40625q1.09375 -1.625 3.140625 -1.625q0.890625 0 1.640625 0.328125q0.75 0.3125 1.109375 0.84375q0.375 0.515625 0.53125 1.21875q0.09375 0.46875 0.09375 1.625l0 6.0625l-1.671875 0l0 -6.0q0 -1.015625 -0.203125 -1.515625q-0.1875 -0.515625 -0.6875 -0.8125q-0.5 -0.296875 -1.171875 -0.296875q-1.0625 0 -1.84375 0.671875q-0.765625 0.671875 -0.765625 2.578125l0 5.375l-1.671875 0zm17.125671 -3.171875l1.71875 0.21875q-0.40625 1.5 -1.515625 2.34375q-1.09375 0.828125 -2.8125 0.828125q-2.15625 0 -3.421875 -1.328125q-1.265625 -1.328125 -1.265625 -3.734375q0 -2.484375 1.265625 -3.859375q1.28125 -1.375 3.328125 -1.375q1.984375 0 3.234375 1.34375q1.25 1.34375 1.25 3.796875q0 0.140625 -0.015625 0.4375l-7.34375 0q0.09375 1.625 0.921875 2.484375q0.828125 0.859375 2.0625 0.859375q0.90625 0 1.546875 -0.46875q0.65625 -0.484375 1.046875 -1.546875zm-5.484375 -2.703125l5.5 0q-0.109375 -1.234375 -0.625 -1.859375q-0.796875 -0.96875 -2.078125 -0.96875q-1.140625 0 -1.9375 0.78125q-0.78125 0.765625 -0.859375 2.046875z" fill-rule="nonzero"></path></g></svg>
+
diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png
new file mode 100644
index 00000000000..f4fe99046f0
--- /dev/null
+++ b/doc/ci/img/view_on_env_blob.png
Binary files differ
diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png
new file mode 100644
index 00000000000..47ddb40bdc1
--- /dev/null
+++ b/doc/ci/img/view_on_env_mr.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index f91b9d350f7..db92a4b0d80 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -1,22 +1,44 @@
-# Introduction to pipelines and builds
+# Introduction to pipelines and jobs
>**Note:**
Introduced in GitLab 8.8.
## Pipelines
-A pipeline is a group of [builds][] that get executed in [stages][](batches).
-All of the builds in a stage are executed in parallel (if there are enough
+A pipeline is a group of [jobs][] that get executed in [stages][](batches).
+All of the jobs in a stage are executed in parallel (if there are enough
concurrent [Runners]), and if they all succeed, the pipeline moves on to the
-next stage. If one of the builds fails, the next stage is not (usually)
+next stage. If one of the jobs fails, the next stage is not (usually)
executed.
![Pipelines example](img/pipelines.png)
-## Builds
+## Types of Pipelines
-Builds are individual runs of [jobs]. Not to be confused with a `build` job or
-`build` stage.
+There are three types of pipelines that often use the single shorthand of "pipeline". People often talk about them as if each one is "the" pipeline, but really, they're just pieces of a single, comprehensive pipeline.
+
+![Types of Pipelines](img/types-of-pipelines.svg)
+
+1. **CI Pipeline**: Build and test stages defined in `.gitlab-ci.yml`
+2. **Deploy Pipeline**: Deploy stage(s) defined in `.gitlab-ci.yml` The flow of deploying code to servers through various stages: e.g. development to staging to production
+3. **Project Pipeline**: Cross-project CI dependencies [triggered via API][triggers], particularly for micro-services, but also for complicated build dependencies: e.g. api -> front-end, ce/ee -> omnibus.
+
+## Development Workflows
+
+Pipelines accommodate several development workflows:
+
+1. **Branch Flow** (e.g. different branch for dev, qa, staging, production)
+2. **Trunk-based Flow** (e.g. feature branches and single master branch, possibly with tags for releases)
+3. **Fork-based Flow** (e.g. merge requests come from forks)
+
+Example continuous delivery flow:
+
+![CD Flow](img/pipelines-goal.svg)
+
+## Jobs
+
+Jobs can be defined in the [`.gitlab-ci.yml`][jobs-yaml] file. Not to be
+confused with a `build` job or `build` stage.
## Defining pipelines
@@ -30,11 +52,11 @@ See full [documentation](yaml/README.md#jobs).
You can find the current and historical pipeline runs under **Pipelines** for
your project.
-## Seeing build status
+## Seeing job status
-Clicking on a pipeline will show the builds that were run for that pipeline.
-Clicking on an individual build will show you its build trace, and allow you to
-cancel the build, retry it, or erase the build trace.
+Clicking on a pipeline will show the jobs that were run for that pipeline.
+Clicking on an individual job will show you its job trace, and allow you to
+cancel the job, retry it, or erase the job trace.
## How the pipeline duration is calculated
@@ -69,11 +91,12 @@ total running time should be:
## Badges
-Build status and test coverage report badges are available. You can find their
+Pipeline status and test coverage report badges are available. You can find their
respective link in the [Pipelines settings] page.
-[builds]: #builds
-[jobs]: yaml/README.md#jobs
+[jobs]: #jobs
+[jobs-yaml]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
-[runners]: runners/READM
+[runners]: runners/README.html
[pipelines settings]: ../user/project/pipelines/settings.md
+[triggers]: triggers/README.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c40cdd55ea5..76e86f3e3c3 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,4 +1,4 @@
-# Quick Start
+# Getting started with GitLab CI
>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
@@ -6,16 +6,16 @@ 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
-push triggers your CI [pipeline].
+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
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
@@ -31,13 +31,13 @@ So in brief, the steps needed to have a working CI can be summed up to:
From there on, on every push to your Git repository, the Runner will
automagically start the pipeline and the pipeline will appear under the
-project's `/pipelines` page.
+project's **Pipelines** page.
---
This guide assumes that you:
-- have a working GitLab instance of version 8.0 or higher or are using
+- have a working GitLab instance of version 8.0+r or are using
[GitLab.com](https://gitlab.com)
- have a project in GitLab that you would like to use CI for
@@ -54,7 +54,7 @@ The `.gitlab-ci.yml` file is where you configure what CI does with your project.
It lives in the root of your repository.
On any push to your repository, GitLab will look for the `.gitlab-ci.yml`
-file and start builds on _Runners_ according to the contents of the file,
+file and start jobs on _Runners_ according to the contents of the file,
for that commit.
Because `.gitlab-ci.yml` is in the repository and is version controlled, old
@@ -63,11 +63,12 @@ have different pipelines and jobs, and you have a single source of truth for CI.
You can read more about the reasons why we are using `.gitlab-ci.yml` [in our
blog about it][blog-ci].
-**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
-so you have to pay extra attention to indentation. Always use spaces, not tabs.
-
### Creating a simple `.gitlab-ci.yml` file
+>**Note:**
+`.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
+so you have to pay extra attention to indentation. Always use spaces, not tabs.
+
You need to create a file named `.gitlab-ci.yml` in the root directory of your
repository. Below is an example for a Ruby on Rails project.
@@ -88,7 +89,7 @@ rubocop:
- bundle exec rubocop
```
-This is the simplest possible build configuration that will work for most Ruby
+This is the simplest possible configuration that will work for most Ruby
applications:
1. Define two jobs `rspec` and `rubocop` (the names are arbitrary) with
@@ -98,22 +99,22 @@ applications:
The `.gitlab-ci.yml` file defines sets of jobs with constraints of how and when
they should be run. The jobs are defined as top-level elements with a name (in
our case `rspec` and `rubocop`) and always have to contain the `script` keyword.
-Jobs are used to create builds, which are then picked by
+Jobs are used to create jobs, which are then picked by
[Runners](../runners/README.md) and executed within the environment of the Runner.
What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
-**Pipelines > Builds** in your project.
+a "CI Lint" button to go to this page under **Pipelines ➔ Pipelines** and
+**Pipelines ➔ Jobs** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
-[the documentation on .gitlab-ci.yml](../yaml/README.md).
+[the reference documentation on .gitlab-ci.yml](../yaml/README.md).
### Push `.gitlab-ci.yml` to GitLab
-Once you've created `.gitlab-ci.yml`, you should add it to your git repository
+Once you've created `.gitlab-ci.yml`, you should add it to your Git repository
and push it to GitLab.
```bash
@@ -125,28 +126,27 @@ git push origin master
Now if you go to the **Pipelines** page you will see that the pipeline is
pending.
-You can also go to the **Commits** page and notice the little clock icon next
+You can also go to the **Commits** page and notice the little pause icon next
to the commit SHA.
![New commit pending](img/new_commit.png)
-Clicking on the clock icon you will be directed to the builds page for that
-specific commit.
+Clicking on it you will be directed to the jobs page for that specific commit.
-![Single commit builds page](img/single_commit_status_pending.png)
+![Single commit jobs page](img/single_commit_status_pending.png)
Notice that there are two jobs pending which are named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
-yet for these builds.
+yet for these jobs.
-The next step is to configure a Runner so that it picks the pending builds.
+The next step is to configure a Runner so that it picks the pending jobs.
## Configuring a Runner
-In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. A Runner
+In GitLab, Runners run the jobs that you define in `.gitlab-ci.yml`. A Runner
can be a virtual machine, a VPS, a bare-metal machine, a docker container or
even a cluster of containers. GitLab and the Runners communicate through an API,
-so the only requirement is that the Runner's machine has Internet access.
+so the only requirement is that the Runner's machine has [Internet] access.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -155,9 +155,9 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
-**Settings > Runners**. Setting up a Runner is easy and straightforward. The
-official Runner supported by GitLab is written in Go and can be found at
-<https://gitlab.com/gitlab-org/gitlab-ci-multi-runner>.
+**Settings ➔ Runners**. Setting up a Runner is easy and straightforward. The
+official Runner supported by GitLab is written in Go and its documentation
+can be found at <https://docs.gitlab.com/runner/>.
In order to have a functional Runner you need to follow two steps:
@@ -167,28 +167,25 @@ In order to have a functional Runner you need to follow two steps:
Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
-For other types of unofficial Runners written in other languages, see the
-[instructions for the various GitLab Runners](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
-
Once the Runner has been set up, you should see it on the Runners page of your
-project, following **Settings > Runners**.
+project, following **Settings ➔ Runners**.
![Activated runners](img/runners_activated.png)
### Shared Runners
-If you use [GitLab.com](https://gitlab.com/) you can use **Shared Runners**
+If you use [GitLab.com](https://gitlab.com/) you can use the **Shared Runners**
provided by GitLab Inc.
These are special virtual machines that run on GitLab's infrastructure and can
build any project.
-To enable **Shared Runners** you have to go to your project's
-**Settings > Runners** and click **Enable shared runners**.
+To enable the **Shared Runners** you have to go to your project's
+**Settings ➔ Runners** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
-## Seeing the status of your pipeline and builds
+## Seeing the status of your pipeline and jobs
After configuring the Runner successfully, you should see the status of your
last commit change from _pending_ to either _running_, _success_ or _failed_.
@@ -197,36 +194,34 @@ You can view all pipelines by going to the **Pipelines** page in your project.
![Commit status](img/pipelines_status.png)
-Or you can view all builds, by going to the **Pipelines > Builds** page.
+Or you can view all jobs, by going to the **Pipelines ➔ Jobs** page.
![Commit status](img/builds_status.png)
-By clicking on a Build ID, you will be able to see the log of that build.
-This is important to diagnose why a build failed or acted differently than
+By clicking on a job's status, you will be able to see the log of that job.
+This is important to diagnose why a job failed or acted differently than
you expected.
![Build log](img/build_log.png)
You are also able to view the status of any commit in the various pages in
-GitLab, such as **Commits** and **Merge Requests**.
+GitLab, such as **Commits** and **Merge requests**.
## Enabling build emails
If you want to receive e-mail notifications about the result status of the
-builds, you should explicitly enable the **Builds Emails** service under your
+jobs, you should explicitly enable the **Builds Emails** service under your
project's settings.
For more information read the
-[Builds emails service documentation](../../project_services/builds_emails.md).
+[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
-Awesome! You started using CI in GitLab!
-
-[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
+[runner-install]: https://docs.gitlab.com/runner/install/
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md
[ci]: https://about.gitlab.com/gitlab-ci/
@@ -235,3 +230,4 @@ Awesome! You started using CI in GitLab!
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
[pipeline]: ../pipelines.md
+[internet]: https://about.gitlab.com/images/theinternet.png
diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png
index 87643d62d58..3a7248ca772 100644
--- a/doc/ci/quick_start/img/build_log.png
+++ b/doc/ci/quick_start/img/build_log.png
Binary files differ
diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png
index d287ae3064f..f829240f3b3 100644
--- a/doc/ci/quick_start/img/builds_status.png
+++ b/doc/ci/quick_start/img/builds_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png
index 29c2fea5d6d..b3dd848b294 100644
--- a/doc/ci/quick_start/img/new_commit.png
+++ b/doc/ci/quick_start/img/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png
index 53ccc49bd66..06d1559f5d2 100644
--- a/doc/ci/quick_start/img/pipelines_status.png
+++ b/doc/ci/quick_start/img/pipelines_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png
index 5ce6fe8e17c..cd83c1a7e4c 100644
--- a/doc/ci/quick_start/img/runners_activated.png
+++ b/doc/ci/quick_start/img/runners_activated.png
Binary files differ
diff --git a/doc/ci/quick_start/img/single_commit_status_pending.png b/doc/ci/quick_start/img/single_commit_status_pending.png
index 91fc9011847..ffc7054d3b0 100644
--- a/doc/ci/quick_start/img/single_commit_status_pending.png
+++ b/doc/ci/quick_start/img/single_commit_status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png
deleted file mode 100644
index cbd44a189d3..00000000000
--- a/doc/ci/quick_start/img/status_pending.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index ddebd987650..1bd1ee93ac5 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,11 +1,11 @@
# Runners
In GitLab CI, Runners run your [yaml](../yaml/README.md).
-A runner is an isolated (virtual) machine that picks up builds
+A Runner is an isolated (virtual) machine that picks up jobs
through the coordinator API of GitLab CI.
-A runner can be specific to a certain project or serve any project
-in GitLab CI. A runner that serves all projects is called a shared runner.
+A Runner can be specific to a certain project or serve any project
+in GitLab CI. A Runner that serves all projects is called a shared Runner.
Ideally, GitLab Runner should not be installed on the same machine as GitLab.
Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
@@ -13,150 +13,150 @@ for more information.
## Shared vs. Specific Runners
-A runner that is specific only runs for the specified project. A shared runner
-can run jobs for every project that has enabled the option
-`Allow shared runners`.
+A Runner that is specific only runs for the specified project. A shared Runner
+can run jobs for every project that has enabled the option **Allow shared Runners**.
-**Shared runners** are useful for jobs that have similar requirements,
-between multiple projects. Rather than having multiple runners idling for
-many projects, you can have a single or a small number of runners that handle
-multiple projects. This makes it easier to maintain and update runners.
+**Shared Runners** are useful for jobs that have similar requirements,
+between multiple projects. Rather than having multiple Runners idling for
+many projects, you can have a single or a small number of Runners that handle
+multiple projects. This makes it easier to maintain and update Runners.
-**Specific runners** are useful for jobs that have special requirements or for
+**Specific Runners** are useful for jobs that have special requirements or for
projects with a specific demand. If a job has certain requirements, you can set
-up the specific runner with this in mind, while not having to do this for all
-runners. For example, if you want to deploy a certain project, you can setup
-a specific runner to have the right credentials for this.
+up the specific Runner with this in mind, while not having to do this for all
+Runners. For example, if you want to deploy a certain project, you can setup
+a specific Runner to have the right credentials for this.
-Projects with high demand of CI activity can also benefit from using specific runners.
-By having dedicated runners you are guaranteed that the runner is not being held
+Projects with high demand of CI activity can also benefit from using specific Runners.
+By having dedicated Runners you are guaranteed that the Runner is not being held
up by another project's jobs.
-You can set up a specific runner to be used by multiple projects. The difference
-with a shared runner is that you have to enable each project explicitly for
-the runner to be able to run its jobs.
+You can set up a specific Runner to be used by multiple projects. The difference
+with a shared Runner is that you have to enable each project explicitly for
+the Runner to be able to run its jobs.
-Specific runners do not get shared with forked projects automatically.
+Specific Runners do not get shared with forked projects automatically.
A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository.
# Creating and Registering a Runner
-There are several ways to create a runner. Only after creation, upon
+There are several ways to create a Runner. Only after creation, upon
registration its status as Shared or Specific is determined.
-[See the documentation for](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation)
+[See the documentation for](https://docs.gitlab.com/runner/install)
the different methods of installing a Runner instance.
-After installing the runner, you can either register it as `Shared` or as `Specific`.
+After installing the Runner, you can either register it as `Shared` or as `Specific`.
You can only register a Shared Runner if you have admin access to the GitLab instance.
## Registering a Shared Runner
-You can only register a shared runner if you are an admin on the linked
+You can only register a shared Runner if you are an admin on the linked
GitLab instance.
-Grab the shared-runner token on the `admin/runners` page of your GitLab CI
+Grab the shared-Runner token on the `admin/runners` page of your GitLab CI
instance.
![shared token](shared_runner.png)
-Now simply register the runner as any runner:
+Now simply register the Runner as any Runner:
```
sudo gitlab-ci-multi-runner register
```
-Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
-`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to
+Shared Runners are enabled by default as of GitLab 8.2, but can be disabled with the
+`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared Runners to
disabled.
## Registering a Specific Runner
Registering a specific can be done in two ways:
-1. Creating a runner with the project registration token
-1. Converting a shared runner into a specific runner (one-way, admin only)
+1. Creating a Runner with the project registration token
+1. Converting a shared Runner into a specific Runner (one-way, admin only)
-There are several ways to create a runner instance. The steps below only
-concern registering the runner on GitLab CI.
+There are several ways to create a Runner instance. The steps below only
+concern registering the Runner on GitLab CI.
### Registering a Specific Runner with a Project Registration token
-To create a specific runner without having admin rights to the GitLab instance,
-visit the project you want to make the runner work for in GitLab CI.
+To create a specific Runner without having admin rights to the GitLab instance,
+visit the project you want to make the Runner work for in GitLab CI.
-Click on the runner tab and use the registration token you find there to
-setup a specific runner for this project.
+Click on the Runner tab and use the registration token you find there to
+setup a specific Runner for this project.
-![project runners in GitLab CI](project_specific.png)
+![project Runners in GitLab CI](project_specific.png)
-To register the runner, run the command below and follow instructions:
+To register the Runner, run the command below and follow instructions:
```
sudo gitlab-ci-multi-runner register
```
-### Lock a specific runner from being enabled for other projects
+### Lock a specific Runner from being enabled for other projects
-You can configure a runner to assign it exclusively to a project. When a
-runner is locked this way, it can no longer be enabled for other projects.
-This setting is available on each runner in *Project Settings* > *Runners*.
+You can configure a Runner to assign it exclusively to a project. When a
+Runner is locked this way, it can no longer be enabled for other projects.
+This setting is available on each Runner in *Project Settings* > *Runners*.
### Making an existing Shared Runner Specific
If you are an admin on your GitLab instance,
-you can make any shared runner a specific runner, _but you can not
-make a specific runner a shared runner_.
+you can make any shared Runner a specific Runner, _but you can not
+make a specific Runner a shared Runner_.
-To make a shared runner specific, go to the runner page (`/admin/runners`)
-and find your runner. Add any projects on the left to make this runner
-run exclusively for these projects, therefore making it a specific runner.
+To make a shared Runner specific, go to the Runner page (`/admin/runners`)
+and find your Runner. Add any projects on the left to make this Runner
+run exclusively for these projects, therefore making it a specific Runner.
-![making a shared runner specific](shared_to_specific_admin.png)
+![making a shared Runner specific](shared_to_specific_admin.png)
## Using Shared Runners Effectively
-If you are planning to use shared runners, there are several things you
+If you are planning to use shared Runners, there are several things you
should keep in mind.
### Use Tags
-You must setup a runner to be able to run all the different types of jobs
+You must setup a Runner to be able to run all the different types of jobs
that it may encounter on the projects it's shared over. This would be
problematic for large amounts of projects, if it wasn't for tags.
By tagging a Runner for the types of jobs it can handle, you can make sure
-shared runners will only run the jobs they are equipped to run.
+shared Runners will only run the jobs they are equipped to run.
-For instance, at GitLab we have runners tagged with "rails" if they contain
+For instance, at GitLab we have Runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites.
-### Prevent runner with tags from picking jobs without tags
+### Prevent Runner with tags from picking jobs without tags
-You can configure a runner to prevent it from picking jobs with tags when
-the runner does not have tags assigned. This setting is available on each
-runner in *Project Settings* > *Runners*.
+You can configure a Runner to prevent it from picking jobs with tags when
+the Runner does not have tags assigned. This setting is available on each
+Runner in *Project Settings* > *Runners*.
### Be careful with sensitive information
-If you can run a build on a runner, you can get access to any code it runs
-and get the token of the runner. With shared runners, this means that anyone
-that runs jobs on the runner, can access anyone else's code that runs on the runner.
+If you can run a job on a Runner, you can get access to any code it runs
+and get the token of the Runner. With shared Runners, this means that anyone
+that runs jobs on the Runner, can access anyone else's code that runs on the Runner.
-In addition, because you can get access to the runner token, it is possible
-to create a clone of a runner and submit false builds, for example.
+In addition, because you can get access to the Runner token, it is possible
+to create a clone of a Runner and submit false jobs, for example.
-The above is easily avoided by restricting the usage of shared runners
+The above is easily avoided by restricting the usage of shared Runners
on large public GitLab instances and controlling access to your GitLab instance.
### Forks
Whenever a project is forked, it copies the settings of the jobs that relate
-to it. This means that if you have shared runners setup for a project and
-someone forks that project, the shared runners will also serve jobs of this
+to it. This means that if you have shared Runners setup for a project and
+someone forks that project, the shared Runners will also serve jobs of this
project.
## Attack vectors in Runners
-Mentioned briefly earlier, but the following things of runners can be exploited.
-We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md).
+Mentioned briefly earlier, but the following things of Runners can be exploited.
+We're always looking for contributions that can mitigate these
+[Security Considerations](https://docs.gitlab.com/runner/security/).
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index aaf3aa77837..338368dbbc9 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -31,7 +31,7 @@ Database: el_duderino
```
If you are wondering why we used `mysql` for the `Host`, read more at
-[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
+[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-mysql].
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
@@ -112,7 +112,7 @@ convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
available [shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
-moments the changes will be picked by a public runner and the build will begin.
+moments the changes will be picked by a public runner and the job will begin.
[hub-mysql]: https://hub.docker.com/r/_/mysql/
[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index f787cc0a124..3899b555f32 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -31,7 +31,7 @@ Database: nice_marmot
```
If you are wondering why we used `postgres` for the `Host`, read more at
-[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
+[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-pg].
For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`.
@@ -108,7 +108,7 @@ convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
available [shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
-moments the changes will be picked by a public runner and the build will begin.
+moments the changes will be picked by a public runner and the job will begin.
[hub-pg]: https://hub.docker.com/r/_/postgres/
[postgres-example-repo]: https://gitlab.com/gitlab-examples/postgres
diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md
index 80705024d2f..554c321fd0c 100644
--- a/doc/ci/services/redis.md
+++ b/doc/ci/services/redis.md
@@ -63,7 +63,7 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
-moments the changes will be picked by a public runner and the build will begin.
+moments the changes will be picked by a public runner and the job will begin.
[hub-redis]: https://hub.docker.com/r/_/redis/
[redis-example-repo]: https://gitlab.com/gitlab-examples/redis
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index b858029d25e..d00faaadc8b 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -25,13 +25,13 @@ This is the universal solution which works with any type of executor
1. Create a new SSH key pair with [ssh-keygen][]
2. Add the private key as a **Secret Variable** to the project
-3. Run the [ssh-agent][] during build to load the private key.
+3. Run the [ssh-agent][] during job to load the private key.
## 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`
@@ -77,7 +77,7 @@ SSH key.
You can generate the SSH key from the machine that GitLab Runner is installed
on, and use that key for all projects that are run on this machine.
-First, you need to login to the server that runs your builds.
+First, you need to login to the server that runs your jobs.
Then from the terminal login as the `gitlab-runner` user and generate the SSH
key pair as described in the [SSH keys documentation](../../ssh/README.md).
@@ -103,7 +103,7 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available
[shared runners](../runners/README.md).
Want to hack on it? Simply fork it, commit and push your changes. Within a few
-moments the changes will be picked by a public runner and the build will begin.
+moments the changes will be picked by a public runner and the job will begin.
[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen
[ssh-agent]: http://linux.die.net/man/1/ssh-agent
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index efca05af7b8..ccaee33dc92 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,19 +1,19 @@
-# Triggering Builds through the API
-
-> [Introduced][ci-229] in GitLab CE 7.14.
+# Triggering jobs through the API
> **Note**:
-GitLab 8.12 has a completely redesigned build permissions system.
-Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
+- [Introduced][ci-229] in GitLab CE 7.14.
+- GitLab 8.12 has a completely redesigned job permissions system. Read all
+ about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
## Add a trigger
-You can add a new trigger by going to your project's **Settings > Triggers**.
-The **Add trigger** button will create a new token which you can then use to
-trigger a rebuild of this particular project.
+You can add a new trigger by going to your project's
+**Settings ➔ CI/CD Pipelines ➔ Triggers**. The **Add trigger** button will
+create a new token which you can then use to trigger a rerun of this
+particular project's pipeline.
Every new trigger you create, gets assigned a different token which you can
then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
@@ -27,50 +27,51 @@ You can revoke a trigger any time by going at your project's
**Settings > Triggers** and hitting the **Revoke** button. The action is
irreversible.
-## Trigger a build
+## Trigger a job
> **Note**:
Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
-it will not trigger a build.
+it will not trigger a job.
-To trigger a build you need to send a `POST` request to GitLab's API endpoint:
+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
the trigger will be performed. Valid refs are the branch and the tag. The `:id`
of a project can be found by [querying the API](../../api/projects.md)
-or by visiting the **Triggers** page which provides self-explanatory examples.
+or by visiting the **CI/CD Pipelines** settings page which provides
+self-explanatory examples.
-When a rebuild is triggered, the information is exposed in GitLab's UI under
-the **Builds** page and the builds are marked as `triggered`.
+When a rerun of a pipeline is triggered, the information is exposed in GitLab's
+UI under the **Jobs** page and the jobs are marked as triggered 'by API'.
-![Marked rebuilds as triggered on builds page](img/builds_page.png)
+![Marked rebuilds as on jobs page](img/builds_page.png)
---
-You can see which trigger caused the rebuild by visiting the single build page.
-The token of the trigger is exposed in the UI as you can see from the image
+You can see which trigger caused the rebuild by visiting the single job page.
+A part of the trigger's token is exposed in the UI as you can see from the image
below.
-![Marked rebuilds as triggered on a single build page](img/trigger_single_build.png)
+![Marked rebuilds as triggered on a single job page](img/trigger_single_build.png)
---
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
-## Trigger a build from webhook
+## Trigger a job from webhook
> Introduced in GitLab 8.14.
-To trigger a build from webhook of another project you need to add the following
+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**:
@@ -78,7 +79,7 @@ https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOK
from webhook body that designates the branchref that fired the trigger in the source repository.
- `ref` should be url encoded if contains slashes.
-## Pass build variables to a trigger
+## Pass job variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
@@ -90,7 +91,7 @@ variables[key]=value
This information is also exposed in the UI.
-![Build variables in UI](img/trigger_variables.png)
+![Job variables in UI](img/trigger_variables.png)
---
@@ -104,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.
@@ -113,10 +114,10 @@ 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 build within `.gitlab-ci.yml`
+### Triggering a job within `.gitlab-ci.yml`
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
you have two projects, A and B, and you want to trigger a rebuild on the `master`
@@ -127,12 +128,12 @@ 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
```
-Now, whenever a new tag is pushed on project A, the build will run and the
+Now, whenever a new tag is pushed on project A, the job will run and the
`build_docs` job will be executed, triggering a rebuild of project B. The
`stage: deploy` ensures that this job will run only after all jobs with
`stage: test` complete successfully.
@@ -186,25 +187,25 @@ 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 builds
+### Using webhook to trigger job
-You can add the following webhook to another project in order to trigger a build:
+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 builds
+### Using cron to trigger nightly jobs
-Whether you craft a script or just run cURL directly, you can trigger builds
-in conjunction with cron. The example below triggers a build on the `master`
+Whether you craft a script or just run cURL directly, you can trigger jobs
+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/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index fded5839f76..c9cc8f308f4 100644
--- a/doc/ci/triggers/img/builds_page.png
+++ b/doc/ci/triggers/img/builds_page.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index c4a5550d640..837bbeffe9f 100644
--- a/doc/ci/triggers/img/trigger_single_build.png
+++ b/doc/ci/triggers/img/trigger_single_build.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 65fe1ea9ab6..0c2a761cfa9 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 56d13905ce6..8ebf68d0384 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d3b9611b02e..03e6b5303c5 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,6 +1,6 @@
# Variables
-When receiving a build from GitLab CI, the [Runner] prepares the build environment.
+When receiving a job from GitLab CI, the [Runner] prepares the build environment.
It starts by setting a list of **predefined variables** (environment variables)
and a list of **user-defined variables**.
@@ -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 build is executed in CI environment |
-| **GITLAB_CI** | all | all | Mark that build is executed in GitLab CI environment |
-| **CI_SERVER** | all | all | Mark that build is executed in CI environment |
-| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate builds |
-| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule builds |
-| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule builds |
-| **CI_BUILD_ID** | all | all | The unique id of the current build 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 build 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 build was [triggered] |
-| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that build 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 build is run |
-| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build |
-| **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 build |
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a build |
-| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a build |
-| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
-| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
-
-
-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,12 +125,22 @@ 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:**
- This feature requires GitLab Runner 0.4.0 or higher.
- Be aware that secret variables are not masked, and their values can be shown
- in the build logs if explicitly asked to do so. If your project is public or
+ in the job logs if explicitly asked to do so. If your project is public or
internal, you can set the pipelines private from your project's Pipelines
settings. Follow the discussion in issue [#13784][ce-13784] for masking the
secret variables.
@@ -148,23 +152,24 @@ 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 builds.
+Once you set them, they will be available for all subsequent jobs.
## Deployment variables
>**Note:**
This feature requires GitLab CI 8.15 or higher.
-[Project services](../../project_services/project_services.md) that are
+[Project services](../../user/project/integrations/project_services.md) that are
responsible for deployment configuration may define their own variables that
are set in the build environment. These variables are only defined for
-[deployment builds](../environments.md). Please consult the documentation of
+[deployment jobs](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
-[Kubernetes Service](../../project_services/kubernetes.md).
+[Kubernetes Service](../../user/project/integrations/kubernetes.md).
## Debug tracing
@@ -173,21 +178,21 @@ An example project service that defines deployment variables is
> **WARNING:** Enabling debug tracing can have severe security implications. The
output **will** contain the content of all your secret variables and any other
secrets! The output **will** be uploaded to the GitLab server and made visible
- in build traces!
+ in job traces!
By default, GitLab Runner hides most of the details of what it is doing when
-processing a job. This behaviour keeps build traces short, and prevents secrets
+processing a job. This behaviour keeps job traces short, and prevents secrets
from being leaked into the trace unless your script writes them to the screen.
If a job isn't working as expected, this can make the problem difficult to
investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
Available on GitLab Runner v1.7+, this feature enables the shell's execution
-trace, resulting in a verbose build trace listing all commands that were run,
+trace, resulting in a verbose job trace listing all commands that were run,
variables that were set, etc.
-Before enabling this, you should ensure builds are visible to
+Before enabling this, you should ensure jobs are visible to
[team members only](../../user/permissions.md#project-features). You should
-also [erase](../pipelines.md#seeing-build-status) all generated build traces
+also [erase](../pipelines.md#seeing-build-status) all generated job traces
before making them visible again.
To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
@@ -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
@@ -320,7 +325,7 @@ MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw'
All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
-In most cases `bash` or `sh` is used to execute the build script.
+In most cases `bash` or `sh` is used to execute the job script.
To access the variables (predefined and user-defined) in a `bash`/`sh` environment,
prefix the variable name with the dollar sign (`$`):
@@ -328,12 +333,12 @@ prefix the variable name with the dollar sign (`$`):
```
job_name:
script:
- - echo $CI_BUILD_ID
+ - echo $CI_job_ID
```
You can also list all environment variables with the `export` command,
but be aware that this will also expose the values of all the secret variables
-you set, in the build log:
+you set, in the job log:
```
job_name:
@@ -341,7 +346,42 @@ 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
-[triggers]: ../triggers/README.md#pass-build-variables-to-a-trigger
+[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 06810898cfe..49fa8761e5e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1,7 +1,7 @@
-# Configuration of your builds with .gitlab-ci.yml
+# Configuration of your jobs with .gitlab-ci.yml
This document describes the usage of `.gitlab-ci.yml`, the file that is used by
-GitLab Runner to manage your project's builds.
+GitLab Runner to manage your project's jobs.
If you want a quick introduction to GitLab CI, follow our
[quick start guide](../quick_start/README.md).
@@ -30,10 +30,9 @@ jobs, where each of the jobs executes a different command.
Of course a command can execute code directly (`./configure;make;make install`)
or run a script (`test.sh`) in the repository.
-Jobs are used to create builds, which are then picked up by
-[Runners](../runners/README.md) and executed within the environment of the
-Runner. What is important, is that each job is run independently from each
-other.
+Jobs are picked up by [Runners](../runners/README.md) and executed within the
+environment of the Runner. What is important, is that each job is run
+independently from each other.
The YAML syntax allows for using more complex job specifications than in the
above example:
@@ -71,40 +70,40 @@ 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 |
| cache | no | Define list of files that should be cached between subsequent runs |
-| coverage | no | Define coverage settings for all jobs |
### image and services
This allows to specify a custom Docker image and a list of services that can be
-used for time of the build. The configuration of this feature is covered in
+used for time of the job. The configuration of this feature is covered in
[a separate document](../docker/README.md).
### before_script
`before_script` is used to define the command that should be run before all
-builds, including deploy builds, but after the restoration of artifacts. This can be an array or a multi-line string.
+jobs, including deploy jobs, but after the restoration of artifacts. This can
+be an array or a multi-line string.
### after_script
> Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
`after_script` is used to define the command that will be run after for all
-builds. This has to be an array or a multi-line string.
+jobs. This has to be an array or a multi-line string.
### stages
-`stages` is used to define build stages that can be used by jobs.
+`stages` is used to define stages that can be used by jobs.
The specification of `stages` allows for having flexible multi stage pipelines.
-The ordering of elements in `stages` defines the ordering of builds' execution:
+The ordering of elements in `stages` defines the ordering of jobs' execution:
-1. Builds of the same stage are run in parallel.
-1. Builds of the next stage are run after the jobs from the previous stage
+1. Jobs of the same stage are run in parallel.
+1. Jobs of the next stage are run after the jobs from the previous stage
complete successfully.
Let's consider the following example, which defines 3 stages:
@@ -116,7 +115,7 @@ stages:
- deploy
```
-1. First all jobs of `build` are executed in parallel.
+1. First, all jobs of `build` are executed in parallel.
1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
1. If all jobs of `deploy` succeed, the commit is marked as `success`.
@@ -125,12 +124,14 @@ stages:
There are also two edge cases worth mentioning:
-1. If no `stages` are defined in `.gitlab-ci.yml`, then by default the `build`,
+1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`,
`test` and `deploy` are allowed to be used as job's stage by default.
2. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
### types
+> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead.
+
Alias for [stages](#stages).
### variables
@@ -138,7 +139,7 @@ Alias for [stages](#stages).
> Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
-build environment. The variables are stored in the Git repository and are meant
+job environment. The variables are stored in the Git repository and are meant
to store non-sensitive project configuration, for example:
```yaml
@@ -164,13 +165,14 @@ which can be set in GitLab's UI.
> Introduced in GitLab Runner v0.7.0.
`cache` is used to specify a list of files and directories which should be
-cached between builds. You can only use paths that are within the project
+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`:
@@ -203,8 +205,8 @@ rspec:
- binaries/
```
-Locally defined cache overwrites globally defined options. This will cache only
-`binaries/`:
+Locally defined cache overrides globally defined options. The following `rspec`
+job will cache only `binaries/`:
```yaml
cache:
@@ -214,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.
@@ -234,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**
@@ -279,28 +289,11 @@ cache:
untracked: true
```
-### coverage
-
-`coverage` allows you to configure how coverage will be filtered out from the
-build outputs. Setting this up globally will make all the jobs to use this
-setting for output filtering and extracting the coverage information from your
-builds.
-
-Regular expressions are the only valid kind of value expected here. So, using
-surrounding `/` is mandatory in order to consistently and explicitly represent
-a regular expression string. You must escape special characters if you want to
-match them literally.
-
-A simple example:
-```yaml
-coverage: /\(\d+\.\d+\) covered\./
-```
-
## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
-must have a unique name, which is not one of the Keywords mentioned above.
-A job is defined by a list of parameters that define the build behavior.
+must have a unique name, which is not one of the keywords mentioned above.
+A job is defined by a list of parameters that define the job behavior.
```yaml
job_name:
@@ -320,24 +313,24 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
-| script | yes | Defines a shell script which is executed by Runner |
-| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
-| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
-| stage | no | Defines a build stage (default: `test`) |
-| type | no | Alias for `stage` |
-| variables | no | Define build variables on a job level |
-| only | no | Defines a list of git refs for which build is created |
-| except | no | Defines a list of git refs for which build is not created |
-| tags | no | Defines a list of tags which are used to select Runner |
-| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
-| when | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
-| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
-| artifacts | no | Define list of build artifacts |
-| cache | no | Define list of files that should be cached between subsequent runs |
-| before_script | no | Override a set of commands that are executed before build |
-| after_script | no | Override a set of commands that are executed after build |
-| environment | no | Defines a name of environment to which deployment is done by this build |
-| coverage | no | Define coverage settings for a given job |
+| script | yes | Defines a shell script which is executed by Runner |
+| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| stage | no | Defines a job stage (default: `test`) |
+| type | no | Alias for `stage` |
+| variables | no | Define job variables on a job level |
+| only | no | Defines a list of git refs for which job is created |
+| except | no | Defines a list of git refs for which job is not created |
+| tags | no | Defines a list of tags which are used to select Runner |
+| allow_failure | no | Allow job to fail. Failed job doesn't contribute to commit status |
+| when | no | Define when to run job. Can be `on_success`, `on_failure`, `always` or `manual` |
+| dependencies | no | Define other jobs that a job depends on so that you can pass artifacts between them|
+| artifacts | no | Define list of [job artifacts](../../user/project/pipelines/job_artifacts.md) |
+| cache | no | Define list of files that should be cached between subsequent runs |
+| before_script | no | Override a set of commands that are executed before job |
+| after_script | no | Override a set of commands that are executed after job |
+| environment | no | Defines a name of environment to which deployment is done by this job |
+| coverage | no | Define code coverage settings for a given job |
### script
@@ -357,11 +350,15 @@ job:
- bundle exec rspec
```
-Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``).
+Sometimes, `script` commands will need to be wrapped in single or double quotes.
+For example, commands that contain a colon (`:`) need to be wrapped in quotes so
+that the YAML parser knows to interpret the whole thing as a string rather than
+a "key: value" pair. Be careful when using special characters:
+`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``.
### stage
-`stage` allows to group build into different stages. Builds of the same `stage`
+`stage` allows to group jobs into different stages. Jobs of the same `stage`
are executed in `parallel`. For more info about the use of `stage` please check
[stages](#stages).
@@ -370,10 +367,9 @@ are executed in `parallel`. For more info about the use of `stage` please check
`only` and `except` are two parameters that set a refs policy to limit when
jobs are built:
-1. `only` defines the names of branches and tags for which the job will be
- built.
+1. `only` defines the names of branches and tags for which the job will run.
2. `except` defines the names of branches and tags for which the job will
- **not** be built.
+ **not** run.
There are a few rules that apply to the usage of refs policy:
@@ -397,8 +393,8 @@ job:
- branches
```
-In this example, `job` will run only for refs that are tagged, or if a build is explicitly requested
-via an API trigger.
+In this example, `job` will run only for refs that are tagged, or if a build is
+explicitly requested via an API trigger.
```yaml
job:
@@ -422,14 +418,14 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except master.
-### job variables
+### Job variables
-It is possible to define build variables using a `variables` keyword on a job
-level. It works basically the same way as its [global-level equivalent](#variables)
-but allows you to define job-specific build variables.
+It is possible to define job variables using a `variables` keyword on a job
+level. It works basically the same way as its [global-level equivalent](#variables),
+but allows you to define job-specific variables.
-When the `variables` keyword is used on a job level, it overrides global YAML
-build variables and predefined variables. To turn off global defined variables
+When the `variables` keyword is used on a job level, it overrides the global YAML
+job variables and predefined ones. To turn off global defined variables
in your job, define an empty array:
```yaml
@@ -437,8 +433,7 @@ job_name:
variables: []
```
-Build variables priority is defined in the
-[variables documentation][variables].
+Job variables priority is defined in the [variables documentation][variables].
### tags
@@ -448,7 +443,7 @@ allowed to run this project.
During the registration of a Runner, you can specify the Runner's tags, for
example `ruby`, `postgres`, `development`.
-`tags` allow you to run builds with Runners that have the specified tags
+`tags` allow you to run jobs with Runners that have the specified tags
assigned to them:
```yaml
@@ -463,13 +458,13 @@ has both `ruby` AND `postgres` tags defined.
### allow_failure
-`allow_failure` is used when you want to allow a build to fail without impacting
-the rest of the CI suite. Failed builds don't contribute to the commit status.
+`allow_failure` is used when you want to allow a job to fail without impacting
+the rest of the CI suite. Failed jobs don't contribute to the commit status.
-When enabled and the build fails, the pipeline will be successful/green for all
+When enabled and the job fails, the pipeline will be successful/green for all
intents and purposes, but a "CI build passed with warnings" message will be
-displayed on the merge request or commit or build page. This is to be used by
-builds that are allowed to fail, but where failure indicates some other (manual)
+displayed on the merge request or commit or job page. This is to be used by
+jobs that are allowed to fail, but where failure indicates some other (manual)
steps should be taken elsewhere.
In the example below, `job1` and `job2` will run in parallel, but if `job1`
@@ -501,12 +496,12 @@ failure.
`when` can be set to one of the following values:
-1. `on_success` - execute build only when all builds from prior stages
+1. `on_success` - execute job only when all jobs from prior stages
succeed. This is the default.
-1. `on_failure` - execute build only when at least one build from prior stages
+1. `on_failure` - execute job only when at least one job from prior stages
fails.
-1. `always` - execute build regardless of the status of builds from prior stages.
-1. `manual` - execute build manually (added in GitLab 8.10). Read about
+1. `always` - execute job regardless of the status of jobs from prior stages.
+1. `manual` - execute job manually (added in GitLab 8.10). Read about
[manual actions](#manual-actions) below.
For example:
@@ -544,7 +539,7 @@ deploy_job:
cleanup_job:
stage: cleanup
script:
- - cleanup after builds
+ - cleanup after jobs
when: always
```
@@ -561,19 +556,37 @@ 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].
-### environment
+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.
-> Introduced in GitLab 8.9.
+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_.
-> You can read more about environments and find more examples in the
-[documentation about environments][environment].
+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
+
+>
+**Notes:**
+- Introduced in GitLab 8.9.
+- You can read more about environments and find more examples in the
+ [documentation about environments][environment].
`environment` is used to define that a job deploys to a specific environment.
If `environment` is specified and no environment under that name exists, a new
@@ -581,7 +594,7 @@ one will be created automatically.
In its simplest form, the `environment` keyword can be defined like:
-```
+```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
@@ -594,12 +607,12 @@ deployment to the `production` environment.
#### environment:name
-> Introduced in GitLab 8.11.
-
->**Note:**
-Before GitLab 8.11, the name of an environment could be defined as a string like
-`environment: production`. The recommended way now is to define it under the
-`name` keyword.
+>
+**Notes:**
+- Introduced in GitLab 8.11.
+- Before GitLab 8.11, the name of an environment could be defined as a string like
+ `environment: production`. The recommended way now is to define it under the
+ `name` keyword.
The `environment` name can contain:
@@ -620,7 +633,7 @@ Instead of defining the name of the environment right after the `environment`
keyword, it is also possible to define it as a separate value. For that, use
the `name` keyword under `environment`:
-```
+```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
@@ -630,11 +643,11 @@ deploy to production:
#### environment:url
-> Introduced in GitLab 8.11.
-
->**Note:**
-Before GitLab 8.11, the URL could be added only in GitLab's UI. The
-recommended way now is to define it in `.gitlab-ci.yml`.
+>
+**Notes:**
+- Introduced in GitLab 8.11.
+- Before GitLab 8.11, the URL could be added only in GitLab's UI. The
+ recommended way now is to define it in `.gitlab-ci.yml`.
This is an optional value that when set, it exposes buttons in various places
in GitLab which when clicked take you to the defined URL.
@@ -643,7 +656,7 @@ In the example below, if the job finishes successfully, it will create buttons
in the merge requests and in the environments/deployments pages which will point
to `https://prod.example.com`.
-```
+```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
@@ -705,11 +718,15 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `when` - [reference](#when)
- `environment:name`
- `environment:action`
+- `stage` should be the same as the `review_app` in order for the environment
+ to stop automatically when the branch is deleted
#### dynamic environments
-> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
- `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15
+>
+**Notes:**
+- [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+- The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15.
`environment` can also represent a configuration hash with `name` and `url`.
These parameters can use any of the defined [CI variables](#variables)
@@ -717,7 +734,7 @@ These parameters can use any of the defined [CI variables](#variables)
For example:
-```
+```yaml
deploy as review app:
stage: deploy
script: make deploy
@@ -732,28 +749,27 @@ 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
in a branch named `pow`, this environment would be accessible with an URL like
-`https://review-pow-aaaaaa.example.com/`.
+`https://review-pow.example.com/`.
This of course implies that the underlying server which hosts the application
is properly configured.
The common use case is to create dynamic environments for branches and use them
as Review Apps. You can see a simple example using Review Apps at
-https://gitlab.com/gitlab-examples/review-apps-nginx/.
+<https://gitlab.com/gitlab-examples/review-apps-nginx/>.
### artifacts
->**Notes:**
>
-> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
-> - Windows support was added in GitLab Runner v.1.0.0.
-> - Currently not all executors are supported.
-> - Build artifacts are only collected for successful builds by default.
+**Notes:**
+- Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
+- Windows support was added in GitLab Runner v.1.0.0.
+- Currently not all executors are supported.
+- Job artifacts are only collected for successful jobs by default.
`artifacts` is used to specify a list of files and directories which should be
-attached to the build after success. You can only use paths that are within the
-project workspace. To pass artifacts between different builds, see [dependencies](#dependencies).
-
+attached to the job after success. You can only use paths that are within the
+project workspace. To pass artifacts between different jobs, see [dependencies](#dependencies).
Below are some examples.
Send all files in `binaries` and `.config`:
@@ -812,7 +828,7 @@ release-job:
- tags
```
-The artifacts will be sent to GitLab after a successful build and will
+The artifacts will be sent to GitLab after the job finishes successfully and will
be available for download in the GitLab UI.
#### artifacts:name
@@ -829,7 +845,7 @@ The default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
**Example configurations**
-To create an archive with a name of the current build:
+To create an archive with a name of the current job:
```yaml
job:
@@ -847,7 +863,7 @@ job:
untracked: true
```
-To create an archive with a name of the current build and the current branch or
+To create an archive with a name of the current job and the current branch or
tag including only the files that are untracked by Git:
```yaml
@@ -882,20 +898,20 @@ job:
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
-`artifacts:when` is used to upload artifacts on build failure or despite the
+`artifacts:when` is used to upload artifacts on job failure or despite the
failure.
`artifacts:when` can be set to one of the following values:
-1. `on_success` - upload artifacts only when the build succeeds. This is the default.
-1. `on_failure` - upload artifacts only when the build fails.
-1. `always` - upload artifacts regardless of the build status.
+1. `on_success` - upload artifacts only when the job succeeds. This is the default.
+1. `on_failure` - upload artifacts only when the job fails.
+1. `always` - upload artifacts regardless of the job status.
---
**Example configurations**
-To upload artifacts only when build fails.
+To upload artifacts only when job fails.
```yaml
job:
@@ -912,13 +928,14 @@ time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
to specify how long artifacts should live before they expire, counting from the
time they are uploaded and stored on GitLab.
-You can use the **Keep** button on the build page to override expiration and
+You can use the **Keep** button on the job page to override expiration and
keep artifacts forever.
After expiry, artifacts are actually deleted hourly by default (via a cron job),
but they are not accessible after expiry.
The value of `expire_in` is an elapsed time. Examples of parseable values:
+
- '3 mins 4 sec'
- '2 hrs 20 min'
- '2h20min'
@@ -943,14 +960,14 @@ job:
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
-allows you to define the artifacts to pass between different builds.
+allows you to define the artifacts to pass between different jobs.
Note that `artifacts` from all previous [stages](#stages) are passed by default.
To use this feature, define `dependencies` in context of the job and pass
-a list of all previous builds from which the artifacts should be downloaded.
-You can only define builds from stages that are executed before the current one.
-An error will be shown if you define builds from the current stage or next ones.
+a list of all previous jobs from which the artifacts should be downloaded.
+You can only define jobs from stages that are executed before the current one.
+An error will be shown if you define jobs from the current stage or next ones.
Defining an empty array will skip downloading any artifacts for that job.
---
@@ -960,7 +977,7 @@ In the following example, we define two jobs with artifacts, `build:osx` and
will be downloaded and extracted in the context of the build. The same happens
for `test:linux` and artifacts from `build:linux`.
-The job `deploy` will download artifacts from all previous builds because of
+The job `deploy` will download artifacts from all previous jobs because of
the [stage](#stages) precedence:
```yaml
@@ -997,7 +1014,7 @@ deploy:
### before_script and after_script
-It's possible to overwrite globally defined `before_script` and `after_script`:
+It's possible to overwrite the globally defined `before_script` and `after_script`:
```yaml
before_script:
@@ -1012,25 +1029,26 @@ job:
- execute this after my script
```
-### job coverage
+### coverage
-This entry is pretty much the same as described in the global context in
-[`coverage`](#coverage). The only difference is that, by setting it inside
-the job level, whatever is set in there will take precedence over what has
-been defined in the global level. A quick example of one overriding the
-other would be:
+**Notes:**
+- [Introduced][ce-7447] in GitLab 8.17.
-```yaml
-coverage: /\(\d+\.\d+\) covered\./
+`coverage` allows you to configure how code coverage will be extracted from the
+job output.
+
+Regular expressions are the only valid kind of value expected here. So, using
+surrounding `/` is mandatory in order to consistently and explicitly represent
+a regular expression string. You must escape special characters if you want to
+match them literally.
+A simple example:
+
+```yaml
job1:
- coverage: /Code coverage: \d+\.\d+/
+ coverage: '/Code coverage: \d+\.\d+/'
```
-In the example above, considering the context of the job `job1`, the coverage
-regex that would be used is `/Code coverage: \d+\.\d+/` instead of
-`/\(\d+\.\d+\) covered\./`.
-
## Git Strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
@@ -1047,7 +1065,7 @@ There are three possible values: `clone`, `fetch`, and `none`.
`clone` is the slowest option. It clones the repository from scratch for every
job, ensuring that the project workspace is always pristine.
-```
+```yaml
variables:
GIT_STRATEGY: clone
```
@@ -1056,7 +1074,7 @@ variables:
if it doesn't exist). `git clean` is used to undo any changes made by the last
job, and `git fetch` is used to retrieve commits made since the last job ran.
-```
+```yaml
variables:
GIT_STRATEGY: fetch
```
@@ -1067,7 +1085,7 @@ for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
data may be present, but it is certain to be out of date, so you should only
rely on files brought into the project workspace from cache or artifacts.
-```
+```yaml
variables:
GIT_STRATEGY: none
```
@@ -1081,56 +1099,59 @@ submodules are included when fetching the code before a build. Like
`GIT_STRATEGY`, it can be set in either the global [`variables`](#variables)
section or the [`variables`](#job-variables) section for individual jobs.
-There are three posible values: `none`, `normal`, and `recursive`:
+There are three possible values: `none`, `normal`, and `recursive`:
- `none` means that submodules will not be included when fetching the project
code. This is the default, which matches the pre-v1.10 behavior.
- `normal` means that only the top-level submodules will be included. It is
equivalent to:
+
```
- $ git submodule sync
- $ git submodule update --init
+ git submodule sync
+ git submodule update --init
```
- `recursive` means that all submodules (including submodules of submodules)
will be included. It is equivalent to:
+
```
- $ git submodule sync --recursive
- $ git submodule update --init --recursive
+ git submodule sync --recursive
+ git submodule update --init --recursive
```
Note that for this feature to work correctly, the submodules must be configured
(in `.gitmodules`) with either:
+
- the HTTP(S) URL of a publicly-accessible repository, or
- a relative path to another repository on the same GitLab server. See the
[Git submodules](../git_submodules.md) documentation.
-## Build stages attempts
+## Job stages attempts
> Introduced in GitLab, it requires GitLab Runner v1.9+.
-You can set the number for attempts the running build will try to execute each
+You can set the number for attempts the running job will try to execute each
of the following stages:
-| Variable | Description |
-|-------------------------|-------------|
-| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a build |
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a build |
-| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a build |
+| Variable | Description |
+|-------------------------------- |-------------|
+| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a job |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a job |
+| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a job |
The default is one single attempt.
Example:
-```
+```yaml
variables:
GET_SOURCES_ATTEMPTS: "3"
```
-You can set them in the global [`variables`](#variables) section or the [`variables`](#job-variables)
-section for individual jobs.
+You can set them in the global [`variables`](#variables) section or the
+[`variables`](#job-variables) section for individual jobs.
## Shallow cloning
@@ -1143,21 +1164,22 @@ repositories with a large number of commits or old, large binaries. The value is
passed to `git fetch` and `git clone`.
>**Note:**
-If you use a depth of 1 and have a queue of builds or retry
-builds, jobs may fail.
+If you use a depth of 1 and have a queue of jobs or retry
+jobs, jobs may fail.
-Since Git fetching and cloning is based on a ref, such as a branch name, runners
-can't clone a specific commit SHA. If there are multiple builds in the queue, or
-you are retrying an old build, the commit to be tested needs to be within the
-git history that is cloned. Setting too small a value for `GIT_DEPTH` can make
+Since Git fetching and cloning is based on a ref, such as a branch name, Runners
+can't clone a specific commit SHA. If there are multiple jobs in the queue, or
+you are retrying an old job, the commit to be tested needs to be within the
+Git history that is cloned. Setting too small a value for `GIT_DEPTH` can make
it impossible to run these old commits. You will see `unresolved reference` in
-build logs. You should then reconsider changing `GIT_DEPTH` to a higher value.
+job logs. You should then reconsider changing `GIT_DEPTH` to a higher value.
-Builds that rely on `git describe` may not work correctly when `GIT_DEPTH` is
-set since only part of the git history is present.
+Jobs that rely on `git describe` may not work correctly when `GIT_DEPTH` is
+set since only part of the Git history is present.
To fetch or clone only the last 3 commits:
-```
+
+```yaml
variables:
GIT_DEPTH: "3"
```
@@ -1194,7 +1216,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-YAML also has a handy feature called 'anchors', which let you easily duplicate
+YAML has a handy feature called 'anchors', which lets you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
to provide templates for your jobs.
@@ -1319,15 +1341,44 @@ with an API call.
[Read more in the triggers documentation.](../triggers/README.md)
+### pages
+
+`pages` is a special job that is used to upload static content to GitLab that
+can be used to serve your website. It has a special syntax, so the two
+requirements below must be met:
+
+1. Any static content must be placed under a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+The example below simply moves all files from the root of the project to the
+`public/` directory. The `.public` workaround is so `cp` doesn't also copy
+`public/` to itself in an infinite loop:
+
+```
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+Read more on [GitLab Pages user documentation](../../user/project/pages/index.md).
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
You can find the link under `/ci/lint` of your gitlab instance.
-## Skipping builds
+## Skipping jobs
If your commit message contains `[ci skip]` or `[skip ci]`, using any
-capitalization, the commit will be created but the builds will be skipped.
+capitalization, the commit will be created but the jobs will be skipped.
## Examples
@@ -1341,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/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/code_review.md b/doc/development/code_review.md
index e4a0e0b92bc..819578404b6 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -69,7 +69,7 @@ experience, refactors the existing code). Then:
someone else would be confused by it as well.
- After a round of line notes, it can be helpful to post a summary note such as
"LGTM :thumbsup:", or "Just a couple things to address."
-- Avoid accepting a merge request before the build succeeds. Of course, "Merge
+- Avoid accepting a merge request before the job succeeds. Of course, "Merge
When Pipeline Succeeds" (MWPS) is fine.
- If you set the MR to "Merge When Pipeline Succeeds", you should take over
subsequent revisions for anything that would be spotted after that.
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 75fdf3d8e63..d646de7c54a 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -16,6 +16,22 @@ 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].
+
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
@@ -50,7 +66,7 @@ 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.
-Don't forget to follow [these steps.][page-specific-javascript]
+Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
@@ -238,6 +254,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
@@ -250,23 +269,17 @@ information.
### Running frontend tests
-`rake teaspoon` runs the frontend-only (JavaScript) tests.
+`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
-- `rake teaspoon:fixtures` (re-)generates fixtures
-- `rake teaspoon:tests` actually executes the tests
+- `rake karma:fixtures` (re-)generates fixtures
+- `rake karma:tests` actually executes the tests
-As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
+As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time).
-If you need to debug your tests and/or application code while they're
-running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
-in your browser, open DevTools, and run tests for individual files by clicking
-on them. This is also much faster than setting up and running tests from the
-command line.
-
Please note: Not all of the frontend fixtures are generated. Some are still static
-files. These will not be touched by `rake teaspoon:fixtures`.
+files. These will not be touched by `rake karma:fixtures`.
## Design Patterns
@@ -323,54 +336,13 @@ gl.MyThing = MyThing;
For our currently-supported browsers, see our [requirements][requirements].
-[rails]: http://rubyonrails.org/
-[haml]: http://haml.info/
-[hamlit]: https://github.com/k0kubun/hamlit
-[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
-[scss]: http://sass-lang.com/
-[es6]: https://babeljs.io/
-[sprockets]: https://github.com/rails/sprockets
-[jquery]: https://jquery.com/
-[vue]: http://vuejs.org/
-[vue-docs]: http://vuejs.org/guide/index.html
-[web-page-test]: http://www.webpagetest.org/
-[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
-[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
-[browser-diet]: https://browserdiet.com/
-[d3]: https://d3js.org/
-[chartjs]: http://www.chartjs.org/
-[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
-[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
-[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
-[observatory-cli]: https://github.com/mozilla/http-observatory-cli
-[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
-[secure_headers]: https://github.com/twitter/secureheaders
-[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
-[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
-[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
-[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
-[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
-[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
-[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
-[github-eng-sri]: http://githubengineering.com/subresource-integrity/
-[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
-[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
-[scss-style-guide]: scss_styleguide.md
-[requirements]: ../install/requirements.md#supported-web-browsers
-[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
-[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
-[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
-[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
-[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
## Gotchas
### Spec errors due to use of ES6 features in `.js` files
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
-thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually,
+thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
you may have included `ES6`-style JavaScript in files that don't have the
`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
you're working in (`git mv <file.js> <file.js.es6>`).
@@ -438,3 +410,50 @@ Scenario: Developer can approve merge request
Then I should see approved merge request "Bug NS-04"
```
+
+
+[rails]: http://rubyonrails.org/
+[haml]: http://haml.info/
+[hamlit]: https://github.com/k0kubun/hamlit
+[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
+[scss]: http://sass-lang.com/
+[es6]: https://babeljs.io/
+[sprockets]: https://github.com/rails/sprockets
+[jquery]: https://jquery.com/
+[vue]: http://vuejs.org/
+[vue-docs]: http://vuejs.org/guide/index.html
+[web-page-test]: http://www.webpagetest.org/
+[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
+[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
+[browser-diet]: https://browserdiet.com/
+[d3]: https://d3js.org/
+[chartjs]: http://www.chartjs.org/
+[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
+[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[observatory-cli]: https://github.com/mozilla/http-observatory-cli
+[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
+[secure_headers]: https://github.com/twitter/secureheaders
+[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
+[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
+[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
+[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
+[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
+[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
+[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+[github-eng-sri]: http://githubengineering.com/subresource-integrity/
+[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
+[scss-style-guide]: scss_styleguide.md
+[requirements]: ../install/requirements.md#supported-web-browsers
+[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
+[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
+[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
+[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
+[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
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 0f78e8238af..565d4b33457 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -3,7 +3,7 @@
The purpose of this guide is to document potential "gotchas" that contributors
might encounter or should avoid during development of GitLab CE and EE.
-## Don't `describe` symbols
+## Do not `describe` symbols
Consider the following model spec:
@@ -32,7 +32,7 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth
Except for the top-level `describe` block, always provide a String argument to
`describe`.
-## Don't assert against the absolute value of a sequence-generated attribute
+## Do not assert against the absolute value of a sequence-generated attribute
Consider the following factory:
@@ -121,7 +121,7 @@ describe API::Labels do
end
```
-## Don't `rescue Exception`
+## Do not `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
@@ -130,7 +130,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897
-## Don't use inline JavaScript in views
+## Do not use inline JavaScript in views
Using the inline `:javascript` Haml filters comes with a
performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
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/licensing.md b/doc/development/licensing.md
index 5d177eb26ee..1f115059fb8 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use:
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
+## Requesting Approval for Licenses
+
+Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document.
+
## Notes
Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL.
@@ -96,3 +100,6 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
[OSL]: https://opensource.org/licenses/OSL-3.0
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
+[Org-Repo]: https://gitlab.com/gitlab-com/organization
+[Acceptable-Licenses]: #acceptable-licenses
+[Unacceptable-Licenses]: #unacceptable-licenses
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 568dedf1669..51b4b398f2c 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -2,19 +2,26 @@
This guide contains best-practices for avoiding conflicts between CE and EE.
-## Context
+## Daily CE Upstream merge
-Usually, GitLab Community Edition is merged into the Enterprise Edition once a
-week. During these merges, it's very common to get conflicts when some changes
-in CE do not apply cleanly to EE.
+GitLab Community Edition is merged daily into the Enterprise Edition (look for
+the [`CE Upstream` merge requests]). The daily merge is currently done manually
+by four individuals.
-There are a few things that can help you as a developer to:
+**If a developer pings you in a `CE Upstream` merge request for help with
+resolving conflicts, please help them because it means that you didn't do your
+job to reduce the conflicts nor to ease their resolution in the first place!**
-- know when your merge request to CE will conflict when merged to EE
-- avoid such conflicts in the first place
-- ease future conflict resolutions if conflict is inevitable
+To avoid the conflicts beforehand when working on CE, there are a few tools and
+techniques that can help you:
-## Check the `rake ee_compat_check` in your merge requests
+- know what are the usual types of conflicts and how to prevent them
+- the CI `rake ee_compat_check` job tells you if you need to open an EE-version
+ of your CE merge request
+
+[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
+
+## Check the status of the CI `rake ee_compat_check` job
For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
detect if the current branch's changes will conflict during the CE->EE merge.
@@ -43,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/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/rake_tasks.md b/doc/development/rake_tasks.md
index 827db7e99b8..dcd978c4cd3 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing.
In order to run the test you can use the following commands:
- `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite
-- `rake teaspoon` to run the teaspoon test suite
+- `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests
-Note: Both `rake spinach` and `rake spec` takes significant time to pass.
+Note: Both `rake spinach` and `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running
-a single test or directory related to your changes. After you submit merge request
-CI will run full test suite for you. Green CI status in the merge request means
-full test suite is passed.
+a single test or directory related to your changes. After you submit merge request
+CI will run full test suite for you. Green CI status in the merge request means
+full test suite is passed.
Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
files it can find, also the ones in `/tmp`
diff --git a/doc/development/testing.md b/doc/development/testing.md
index dbea6b9c9aa..9b545d7f0f1 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement.
## JavaScript
-GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on
-the command line via `bundle exec teaspoon`, or via a web browser at
-`http://localhost:3000/teaspoon` when the Rails server is running.
+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
@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
-[Teaspoon]: https://github.com/modeset/teaspoon
+[Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine
## RSpec
@@ -96,6 +95,25 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not
+### Time-sensitive tests
+
+[Timecop](https://github.com/travisjeffery/timecop) is available in our
+Ruby-based tests for verifying things that are time-sensitive. Any test that
+exercises or verifies something time-sensitive should make use of Timecop to
+prevent transient test failures.
+
+Example:
+
+```ruby
+it 'is overdue' do
+ issue = build(:issue, due_date: Date.tomorrow)
+
+ Timecop.freeze(3.days.from_now) do
+ expect(issue).to be_overdue
+ end
+end
+```
+
### Test speed
GitLab has a massive test suite that, without parallelization, can take more
@@ -116,6 +134,10 @@ Here are some things to keep in mind regarding test performance:
### Features / Integration
+GitLab uses [rspec-rails feature specs] to test features in a browser
+environment. These are [capybara] specs running on the headless [poltergeist]
+driver.
+
- Feature specs live in `spec/features/` and should be named
`ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
- Use only one `feature` block per feature spec file.
@@ -123,6 +145,10 @@ Here are some things to keep in mind regarding test performance:
- Avoid scenario titles that add no information, such as "successfully."
- Avoid scenario titles that repeat the feature title.
+[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs
+[capybara]: https://github.com/teamcapybara/capybara
+[poltergeist]: https://github.com/teampoltergeist/poltergeist
+
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 2d1d504202c..df6ac452300 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -20,8 +20,8 @@ The content section contains a header and the content itself. The header describ
available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
-You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
-along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository][gitlab-map-graffle]
+along with [PDF][gitlab-map-pdf] and [PNG][gitlab-map-png] exports.
### Adding new tab to header navigation
@@ -104,4 +104,4 @@ Do not use both green and blue button in one form.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
-[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 1b19587a0b8..18d0647c798 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -96,6 +96,20 @@ Since secondary buttons only have a border on their resting state, their hover a
| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) |
| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) |
+### Placement
+
+When there are a group of buttons in a dialog or a form, we need to be consistent with the placement.
+
+#### Dismissive actions on the left
+The dismissive action returns the user to the previous state.
+
+> Example: Cancel
+
+#### Affirmative actions on the right
+Affirmative actions continue to progress towards the user goal that triggered the dialog or form.
+
+> Example: Submit, Ok, Delete
+
---
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 5b65d531e54..794c8eb6bfe 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -1,193 +1,188 @@
-# Copy
-
-The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
-
-The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
-
-Portions of this page are inspired by work found in the [Material Design guidelines][material design].
-
->**Note:**
-We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
-
-## Contents
-* [Brevity](#brevity)
-* [Capitalization and punctuation](#capitalization-and-punctuation)
-* [Terminology](#terminology)
-
----
-
-## Brevity
-Users will skim content, rather than read text carefully.
-When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly.
-A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text.
-In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab.
-We should _not_ rely on words as a crutch to explain the purpose of a screen.
-The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text.
-This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement.
-
->**Example:**
-Use `Add` instead of `Add issue` as a button label.
-Preferrably use context and placement of controls to make it obvious what clicking on them will do.
-
----
-
-## Capitalization and punctuation
-
-### Case
-Use sentence case for titles, headings, labels, menu items, and buttons. Use title case when referring to [features][features] or [products][products]. Note that some features are also objects (e.g. “Merge Requests” and “merge requests”).
-
-| :white_check_mark: Do | :no_entry_sign: Don’t |
-| --- | --- |
-| Add issues to the Issue Board feature in GitLab Hosted | Add Issues To The Issue Board Feature In GitLab Hosted |
-
-### Avoid periods
-Avoid using periods in solitary sentences in these elements:
-
-* Labels
-* Hover text
-* Bulleted lists
-* Dialog body text
-
-Periods should be used for:
-
-* Lists or dialogs with multiple sentences
-* Any sentence followed by a link
-
-| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
-| --- | --- |
-| Mention someone to notify them. [Learn more](#) | Mention someone to notify them. [Learn more](#). |
-
-| :white_check_mark: **Do** skip periods after solo sentences of body text | :no_entry_sign: **Don’t** place periods after body text if there is only a single sentence |
-| --- | --- |
-| To see the available commands, enter `/gitlab help` | To see the available commands, enter `/gitlab help`. |
-
-### Use contractions
-Don’t make a sentence harder to understand just to follow this rule. For example, “do not” can give more emphasis than “don’t” when needed.
-
-| :white_check_mark: Do | :no_entry_sign: Don’t |
-| --- | --- |
-| it’s, can’t, wouldn’t, you’re, you’ve, haven’t, don’t | it is, cannot, would not, it’ll, should’ve |
-
-### Use numerals for numbers
-Use “1, 2, 3” instead of “one, two, three” for numbers. One exception is when mixing uses of numbers, such as “Enter two 3s.”
-
-| :white_check_mark: Do | :no_entry_sign: Don’t |
-| --- | --- |
-| 3 new commits | Three new commits |
-
-### Punctuation
-Omit punctuation after phrases and labels to create a cleaner and more readable interface. Use punctuation to add clarity or be grammatically correct.
-
-| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
-|---|---|---|---|---|---|---|
-| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
-| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
-| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
-| Colon | **:** | `&#58;` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
-| Apostrophe | **’** | `&rsquo;` | `\u2019` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use for contractions (I’m, you’re, ’89) and to show possession.<br><br>To show possession, add an *’s* to all single nouns and names, even if they already end in an *s*: “Your issues’s status was changed.” For singular proper names ending in *s*, use only an apostrophe: “James’ commits.” For plurals of a single letter, add an *’s*: “Dot your i’s and cross your t’s.”<br><br>Omit for decades or acronyms: “the 1990s”, “MRs.” |
-| Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `&ldquo;`<br><br>`&rdquo;`<br><br>`&lsquo;`<br><br>`&rsquo;` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
-| Primes | **′**<br><br>**″** | `&prime;`<br><br>`&Prime;` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
-| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `&quot;`<br><br>`&#39;`<br><br>`&#96;`<br><br>`&acute;` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
-| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
-| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `&#171;`<br><br>`&#187;`<br><br>`&#8249;`<br><br>`&#8250;`<br><br>`&lt;`<br><br>`&gt;` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
-| Em dash | **—** | `&mdash;` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
-| En dash | **–** | `&ndash;` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
-| Hyphen | **-** | | | | | Use to represent negative numbers, or to avoid ambiguity in adjective-noun or noun-participle pairs. Example: “anti-inflammatory”; “5-mile walk.”<br><br>Omit in commonly understood terms and adverbs that end in *ly*: “frontend”, “greatly improved performance.”<br><br>Omit in the term “open source.” |
-| Parentheses | **( )** | | | | | Use only to define acronyms or jargon: “Secure web connections are based on a technology called SSL (the secure sockets layer).”<br><br>Avoid other uses and instead rewrite the text, or use dashes or commas to set off the information. If parentheses are required: If the parenthetical is a complete, independent sentence, place the period inside the parentheses; if not, the period goes outside. |
-
-When using the <kbd>Alt</kbd> keystrokes in Windows, use the numeric keypad, not the row of numbers above the alphabet, and be sure Num Lock is turned on.
-
----
-
-## Terminology
-Only use the terms in the tables below.
-
-### Projects and Groups
-
-| Term | Use | :no_entry_sign: Don't |
-| ---- | --- | ----- |
-| Members | When discussing the people who are a part of a project or a group. | Don't use `users`. |
-
-### Issues
-
-#### Adjectives (states)
-
-| Term |
-| ---- |
-| Open |
-| Closed |
-| Deleted |
-
->**Example:**
-Use `5 open issues` and don’t use `5 pending issues`.
-
-#### Verbs (actions)
-
-| Term | Use | :no_entry_sign: Don’t |
-| ---- | --- | --- |
-| Add | Add an issue | Don’t use `create` or `new` |
-| View | View an open or closed issue ||
-| Edit | Edit an open or closed issue | Don’t use `update` |
-| Close | Close an open issue ||
-| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb |
-| Delete | Delete an open or closed issue ||
-
-#### Add issue
-
-When viewing a list of issues, there is a button that is labeled `Add`. Given the context in the example, it is clearly referring to issues. If the context were not clear enough, the label could be `Add issue`. Clicking the button will bring you to the `Add issue` form. Other add flows should be similar.
-
-![Add issue button](img/copy-form-addissuebutton.png)
-
-The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don’t use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
-
-![Add issue form](img/copy-form-addissueform.png)
-
-#### Edit issue
-
-When in context of an issue, the affordance to edit it is labeled `Edit`. If the context is not clear enough, `Edit issue` could be considered. Other edit flows should be similar.
-
-![Edit issue button](img/copy-form-editissuebutton.png)
-
-The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don’t use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
-
-![Edit issue form](img/copy-form-editissueform.png)
-
-
-### Merge requests
-
-#### Adjectives (states)
-
-| Term |
-| ---- |
-| Open |
-| Merged |
-
-#### Verbs (actions)
-
-| Term | Use | :no_entry_sign: Don’t |
-| ---- | --- | --- |
-| Add | Add a merge request | Do not use `create` or `new` |
-| View | View an open or merged merge request ||
-| Edit | Edit an open or merged merge request| Do not use `update` |
-| Approve | Approve an open merge request ||
-| Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word|
-| Merge | Merge an open merge request ||
-
-### Comments & Discussions
-
-#### Comment
-A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and timestamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
-
-#### 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.
-
----
-
-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].
-
-[material design]: https://material.io/guidelines/
-[features]: https://about.gitlab.com/features/ "GitLab features page"
-[products]: https://about.gitlab.com/products/ "GitLab products page"
-[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
-[android project]: http://source.android.com/
-[creative commons]: http://creativecommons.org/licenses/by/2.5/ \ No newline at end of file
+# Copy
+
+The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
+
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+
+Portions of this page are inspired by work found in the [Material Design guidelines][material design].
+
+>**Note:**
+We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
+
+## Contents
+* [Brevity](#brevity)
+* [Capitalization and punctuation](#capitalization-and-punctuation)
+* [Terminology](#terminology)
+
+---
+
+## Brevity
+Users will skim content, rather than read text carefully.
+When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly.
+A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text.
+In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab.
+We should _not_ rely on words as a crutch to explain the purpose of a screen.
+The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text.
+This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement.
+
+>**Example:**
+Use `Add` instead of `Add issue` as a button label.
+Preferrably use context and placement of controls to make it obvious what clicking on them will do.
+
+---
+
+## Capitalization and punctuation
+
+### Case
+Use sentence case for titles, headings, labels, menu items, and buttons. Use title case when referring to [features][features] or [products][products]. Note that some features are also objects (e.g. “Merge Requests” and “merge requests”).
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| Add issues to the Issue Board feature in GitLab Hosted | Add Issues To The Issue Board Feature In GitLab Hosted |
+
+### Avoid periods
+Avoid using periods in solitary sentences in these elements:
+
+* Labels
+* Hover text
+* Bulleted lists
+* Dialog body text
+
+Periods should be used for:
+
+* Lists or dialogs with multiple sentences
+* Any sentence followed by a link
+
+| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
+| --- | --- |
+| Mention someone to notify them. [Learn more](#) | Mention someone to notify them. [Learn more](#). |
+
+| :white_check_mark: **Do** skip periods after solo sentences of body text | :no_entry_sign: **Don’t** place periods after body text if there is only a single sentence |
+| --- | --- |
+| To see the available commands, enter `/gitlab help` | To see the available commands, enter `/gitlab help`. |
+
+### Use contractions
+Don’t make a sentence harder to understand just to follow this rule. For example, “do not” can give more emphasis than “don’t” when needed.
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| it’s, can’t, wouldn’t, you’re, you’ve, haven’t, don’t | it is, cannot, would not, it’ll, should’ve |
+
+### Use numerals for numbers
+Use “1, 2, 3” instead of “one, two, three” for numbers. One exception is when mixing uses of numbers, such as “Enter two 3s.”
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| 3 new commits | Three new commits |
+
+### Punctuation
+Omit punctuation after phrases and labels to create a cleaner and more readable interface. Use punctuation to add clarity or be grammatically correct.
+
+| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
+|---|---|---|---|---|---|---|
+| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
+| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
+| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
+| Colon | **:** | `&#58;` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
+| Apostrophe | **’** | `&rsquo;` | `\u2019` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use for contractions (I’m, you’re, ’89) and to show possession.<br><br>To show possession, add an *’s* to all singular common nouns and names, even if they already end in an *s*: “Look into this worker process’s log.” For singular proper names ending in *s*, use only an apostrophe: “James’ commits.” For plurals of a single letter, add an *’s*: “Dot your i’s and cross your t’s.”<br><br>Omit for decades or acronyms: “the 1990s”, “MRs.” |
+| Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `&ldquo;`<br><br>`&rdquo;`<br><br>`&lsquo;`<br><br>`&rsquo;` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
+| Primes | **′**<br><br>**″** | `&prime;`<br><br>`&Prime;` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
+| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `&quot;`<br><br>`&#39;`<br><br>`&#96;`<br><br>`&acute;` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
+| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
+| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `&#171;`<br><br>`&#187;`<br><br>`&#8249;`<br><br>`&#8250;`<br><br>`&lt;`<br><br>`&gt;` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
+| Em dash | **—** | `&mdash;` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
+| En dash | **–** | `&ndash;` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
+| Hyphen | **-** | | | | | Use to represent negative numbers, or to avoid ambiguity in adjective-noun or noun-participle pairs. Example: “anti-inflammatory”; “5-mile walk.”<br><br>Omit in commonly understood terms and adverbs that end in *ly*: “frontend”, “greatly improved performance.”<br><br>Omit in the term “open source.” |
+| Parentheses | **( )** | | | | | Use only to define acronyms or jargon: “Secure web connections are based on a technology called SSL (the secure sockets layer).”<br><br>Avoid other uses and instead rewrite the text, or use dashes or commas to set off the information. If parentheses are required: If the parenthetical is a complete, independent sentence, place the period inside the parentheses; if not, the period goes outside. |
+
+When using the <kbd>Alt</kbd> keystrokes in Windows, use the numeric keypad, not the row of numbers above the alphabet, and be sure Num Lock is turned on.
+
+---
+
+## Terminology
+Only use the terms below.
+
+When using verbs or adjectives:
+* If the context clearly refers to the object, use them alone. Example: `Edit` or `Closed`
+* If the context isn’t clear enough, use them with the object. Example: `Edit issue` or `Closed issues`
+
+### Projects and Groups
+
+| Term | Use | :no_entry_sign: Don't |
+| ---- | --- | ----- |
+| Members | When discussing the people who are a part of a project or a group. | Don't use `users`. |
+
+### Issues
+
+#### Adjectives (states)
+
+| Term | :no_entry_sign: Don’t |
+| ---- | --- |
+| Open | Don’t use `Pending` or `Created` |
+| Closed | Don’t use `Archived` |
+| Deleted | Don’t use `Removed` or `Trashed` |
+
+#### Verbs (actions)
+
+| Term | Use | :no_entry_sign: Don’t |
+| ---- | --- | --- |
+| New | Although it’s not a verb, `New` is a common standard and used for entering the creation mode of a non-existent issue | Don’t use `Create`, `Open`, or `Add` |
+| Create | Only to indicate when or who created an issue ||
+| Add | Associate an existing issue with an item or a list of items | Don’t use `New` or `Create` |
+| View | Open the detail page of an issue | Don’t use `Open` or `See` |
+| Edit | Enter the editing mode of an issue | Don’t use `Change`, `Modify` or `Update` |
+| Submit | Finalize the *creation* process of an issue | Don’t use `Add`, `Create`, `New`, `Open`, `Save` or `Save changes` |
+| Save | Finalize the *editing* process of an issue | Don’t use `Edit`, `Modify`, `Update`, `Submit`, or `Save changes` |
+| Cancel | Cancel the *creation* or *editing* process of an issue | Don’t use `Back`, `Close`, or `Discard` |
+| Close | Close an open issue | Don’t use `Archive` |
+| Re-open | Re-open a closed issue | Don’t use `Open` |
+| Delete | Permanently remove an issue from the system | Don’t use `Remove` |
+| Remove | Remove the association an issue with an item or a list of items | Don’t use `Delete` |
+
+### Merge requests
+
+#### Adjectives (states)
+
+| Term |
+| ---- |
+| Open |
+| Merged |
+
+#### Verbs (actions)
+
+| Term | Use | :no_entry_sign: Don’t |
+| ---- | --- | --- |
+| Add | Add a merge request | Do not use `create` or `new` |
+| View | View an open or merged merge request ||
+| Edit | Edit an open or merged merge request| Do not use `update` |
+| Approve | Approve an open merge request ||
+| Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word|
+| Merge | Merge an open merge request ||
+
+### Comments & Discussions
+
+#### Comment
+A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and timestamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
+
+#### 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].
+
+[material design]: https://material.io/guidelines/
+[features]: https://about.gitlab.com/features/ "GitLab features page"
+[products]: https://about.gitlab.com/products/ "GitLab products page"
+[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
+[android project]: http://source.android.com/
+[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png
deleted file mode 100644
index 8457f0ab2ab..00000000000
--- a/doc/development/ux_guide/img/copy-form-addissuebutton.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png
deleted file mode 100644
index 89c6b4acdfb..00000000000
--- a/doc/development/ux_guide/img/copy-form-addissueform.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png
deleted file mode 100644
index 04bcc2bf831..00000000000
--- a/doc/development/ux_guide/img/copy-form-editissuebutton.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png
deleted file mode 100644
index 126ef34ea7e..00000000000
--- a/doc/development/ux_guide/img/copy-form-editissueform.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png
new file mode 100644
index 00000000000..702a8b02262
--- /dev/null
+++ b/doc/development/ux_guide/img/harry-robison.png
Binary files differ
diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png
new file mode 100644
index 00000000000..6db257c5b39
--- /dev/null
+++ b/doc/development/ux_guide/img/james-mackey.png
Binary files differ
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/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png
new file mode 100644
index 00000000000..2efe1d0b168
--- /dev/null
+++ b/doc/development/ux_guide/img/steven-lyons.png
Binary files differ
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index 717a902c424..cbd7c17de41 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,16 +1,164 @@
-# Users
+## UX Personas
+* [Nazim Ramesh](#nazim-ramesh)
+ - Small to medium size organisations using GitLab CE
+* [James Mackey](#james-mackey)
+ - Medium to large size organisations using CE or EE
+ - Small organisations using EE
+* [Karolina Plaskaty](#karolina-plaskaty)
+ - Using GitLab.com for personal/hobby projects
+ - Would like to use GitLab at work
+ - Working for a medium to large size organisation
-> TODO: Create personas. Understand the similarities and differences across the below spectrums.
+<hr>
-## Users by organization
+### Nazim Ramesh
+- Small to medium size organisations using GitLab CE
-- Enterprise
-- Medium company
-- Small company
-- Open source communities
+<img src="img/nazim-ramesh.png" width="300px">
-## Users by role
+#### Demographics
-- Admin
-- Manager
-- Developer
+- **Age**<br>32 years old
+- **Location**<br>Germany
+- **Education**<br>Bachelor of Science in Computer Science
+- **Occupation**<br>Full-stack web developer
+- **Programming experience**<br>Over 10 years
+- **Frequently used programming languages**<br>JavaScript, SQL, PHP
+- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
+
+#### Motivations
+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, 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, 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.”
+>
+
+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. 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. 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
+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
+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
+>
+“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHub”
+>
+
+##### UI/UX
+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.
+* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
+* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
+
+<hr>
+
+### James Mackey
+- Medium to large size organisations using CE or EE
+- Small organisations using EE
+
+<img src="img/james-mackey.png" width="300px">
+
+#### Demographics
+
+- **Age**<br>36 years old
+- **Location**<br>US
+- **Education**<br>Masters degree in Computer Science
+- **Occupation**<br>Full-stack web developer
+- **Programming experience**<br>Over 10 years
+- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
+- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
+
+#### Motivations
+James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and use”. After using GitLab for over a year, he began to wonder about using it at work. James explains:
+
+>
+“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.”
+>
+
+James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub”.
+
+The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
+
+James feels partially responsible for his organisation’s decision to start using GitLab.
+
+>
+“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.”
+>
+
+#### Frustrations
+##### Third Party Integration
+Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
+
+##### UX/UI
+James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
+
+##### Permissions
+>
+“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.”
+>
+
+#### 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. 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>
+
+### Karolina Plaskaty
+- Using GitLab.com for personal/hobby projects
+- Would like to use GitLab at work
+- Working for a medium to large size organisation
+
+<img src="img/karolina-plaskaty.png" width="300px">
+
+#### Demographics
+
+- **Age**<br>26 years old
+- **Location**<br>UK
+- **Education**<br>Self taught
+- **Occupation**<br>Junior web-developer
+- **Programming experience**<br>6 years
+- **Frequently used programming languages**<br>JavaScript and SQL
+- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
+
+#### Motivations
+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.
+
+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
+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. 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
+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 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/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index 3b075ff5fc0..2a531193adf 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
+>**Note:** If you clone your project via an URL that contains special characters, make sure that they are URL-encoded.
+
### Go into a project, directory or file to work in it
```
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/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
index 26df4c0a734..aaa1a39313d 100644
--- a/doc/gitlab-basics/img/profile_settings.png
+++ b/doc/gitlab-basics/img/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
index 6a1430d9663..7ebb8973ef0 100644
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 239f5f301ec..d35709266e4 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,6 +1,32 @@
# Installation
-- [Installation](installation.md)
-- [Requirements](requirements.md)
-- [Structure](structure.md)
-- [Database MySQL](database_mysql.md)
+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/database_mysql.md b/doc/install/database_mysql.md
index 65bfb0f7d6e..da2dac23c6a 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,64 +1,67 @@
# Database MySQL
-## Note
-
-We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
+>**Note:**
+We do not recommend using MySQL due to various issues. For example, case
+[(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
+and [problems](https://bugs.mysql.com/bug.php?id=65830) that
+[suggested](https://bugs.mysql.com/bug.php?id=50909)
+[fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
## Initial database setup
- # Install the database packages
- sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
-
- # Ensure you have MySQL version 5.5.14 or later
- mysql --version
+```
+# Install the database packages
+sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
- # Pick a MySQL root password (can be anything), type it and press enter
- # Retype the MySQL root password and press enter
+# Ensure you have MySQL version 5.5.14 or later
+mysql --version
- # Secure your installation
- sudo mysql_secure_installation
+# Pick a MySQL root password (can be anything), type it and press enter
+# Retype the MySQL root password and press enter
- # Login to MySQL
- mysql -u root -p
+# Secure your installation
+sudo mysql_secure_installation
- # Type the MySQL root password
+# Login to MySQL
+mysql -u root -p
- # Create a user for GitLab
- # do not type the 'mysql>', this is part of the prompt
- # change $password in the command below to a real password you pick
- mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password';
+# Type the MySQL root password
- # Ensure you can use the InnoDB engine which is necessary to support long indexes
- # If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off"
- mysql> SET storage_engine=INNODB;
+# Create a user for GitLab
+# do not type the 'mysql>', this is part of the prompt
+# change $password in the command below to a real password you pick
+mysql> CREATE USER 'git'@'localhost' IDENTIFIED BY '$password';
- # If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW:
- mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
+# Ensure you can use the InnoDB engine which is necessary to support long indexes
+# If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off"
+mysql> SET storage_engine=INNODB;
- # Create the GitLab production database
- mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`;
+# If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW:
+mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
- # Grant the GitLab user necessary permissions on the database
- mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
+# Create the GitLab production database
+mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`;
- # Quit the database session
- mysql> \q
+# Grant the GitLab user necessary permissions on the database
+mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
- # Try connecting to the new database with the new user
- sudo -u git -H mysql -u git -p -D gitlabhq_production
+# Quit the database session
+mysql> \q
- # Type the password you replaced $password with earlier
+# Try connecting to the new database with the new user
+sudo -u git -H mysql -u git -p -D gitlabhq_production
- # You should now see a 'mysql>' prompt
+# Type the password you replaced $password with earlier
- # Quit the database session
- mysql> \q
+# You should now see a 'mysql>' prompt
- # You are done installing the database for now and can go back to the rest of the installation.
+# Quit the database session
+mysql> \q
+```
+You are done installing the database for now and can go back to the rest of the installation.
Please proceed to the rest of the installation before running through the utf8mb4 support section.
-
### MySQL utf8mb4 support
After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`.
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
new file mode 100644
index 00000000000..820060a489b
--- /dev/null
+++ b/doc/install/digitaloceandocker.md
@@ -0,0 +1,136 @@
+# Digital Ocean and Docker
+
+## Initial setup
+
+In this guide you'll configure a Digital Ocean droplet and set up Docker
+locally on either macOS or Linux.
+
+### On macOS
+
+#### Install Docker Toolbox
+
+1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox)
+
+### On Linux
+
+#### Install Docker Engine
+
+1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/)
+
+#### Install Docker Machine
+
+1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/)
+
+_The rest of the steps are identical for macOS and Linux_
+
+### Create new docker host
+
+1. Login to Digital Ocean
+1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens
+
+
+This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
+
+**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance**
+
++ RAM: 4GB
++ Name: `gitlab-test-env-do`
++ Driver: `digitalocean`
+
+
+**Set the DO token** - Replace the string below with your generated token
+
+```
+export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248
+```
+
+**Create the machine**
+
+```
+docker-machine create \
+ --driver digitalocean \
+ --digitalocean-access-token=$DOTOKEN \
+ --digitalocean-size "4gb" \
+ gitlab-test-env-do
+```
+
++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/
+
+
+### Creating GitLab test instance
+
+
+#### Connect your shell to the new machine
+
+
+In this example we'll create a GitLab EE 8.10.8 instance.
+
+
+First connect the docker client to the docker host you created previously.
+
+```
+eval "$(docker-machine env gitlab-test-env-do)"
+```
+
+You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
+
+
+#### Create new GitLab container
+
++ HTTP port: `8888`
++ SSH port: `2222`
+ + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG `
++ Hostname: IP of docker host
++ Container name: `gitlab-test-8.10`
++ GitLab version: **EE** `8.10.8-ee.0`
+
+##### Setup container settings
+
+```
+export SSH_PORT=2222
+export HTTP_PORT=8888
+export VERSION=8.10.8-ee.0
+export NAME=gitlab-test-8.10
+```
+
+##### Create container
+```
+docker run --detach \
+--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \
+--hostname $(docker-machine ip gitlab-test-env-do) \
+-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \
+--name $NAME \
+gitlab/gitlab-ee:$VERSION
+```
+
+#### Connect to the GitLab container
+
+##### Retrieve the docker host IP
+
+```
+docker-machine ip gitlab-test-env-do
+# example output: 192.168.151.134
+```
+
+
++ Browse to: http://192.168.151.134:8888/
+
+
+##### Execute interactive shell/edit configuration
+
+
+```
+docker exec -it $NAME /bin/bash
+```
+
+```
+# example commands
+root@192:/# vi /etc/gitlab/gitlab.rb
+root@192:/# gitlab-ctl reconfigure
+```
+
+#### Resources
+
++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/)
++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/)
++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+
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 b2d5d51d37d..177e1a9378b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -39,6 +39,7 @@ The GitLab installation consists of setting up the following components:
1. Packages / Dependencies
1. Ruby
1. Go
+1. Node
1. System Users
1. Database
1. Redis
@@ -63,7 +64,7 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
- sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs
+ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake
If you want to use Kerberos for user authentication, then install libkrb5-dev:
@@ -151,13 +152,29 @@ page](https://golang.org/dl).
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
rm go1.5.3.linux-amd64.tar.gz
-## 4. System Users
+## 4. Node
+
+Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile
+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 -
+ sudo apt-get install -y nodejs
+
+ # install yarn
+ curl --location https://yarnpkg.com/install.sh | bash -
+
+Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps.
+
+## 5. System Users
Create a `git` user for GitLab:
sudo adduser --disabled-login --gecos 'GitLab' git
-## 5. Database
+## 6. Database
We recommend using a PostgreSQL database. For MySQL check the
[MySQL setup guide](database_mysql.md).
@@ -218,7 +235,7 @@ We recommend using a PostgreSQL database. For MySQL check the
gitlabhq_production> \q
```
-## 6. Redis
+## 7. Redis
GitLab requires at least Redis 2.8.
@@ -263,7 +280,7 @@ sudo service redis-server restart
sudo usermod -aG redis git
```
-## 7. GitLab
+## 8. GitLab
# We'll install GitLab into home directory of the user "git"
cd /home/git
@@ -271,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-16-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-16-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
@@ -307,12 +324,15 @@ sudo usermod -aG redis git
# now that files in public/uploads are served by gitlab-workhorse
sudo chmod 0700 public/uploads
- # Change the permissions of the directory where CI build traces are stored
+ # Change the permissions of the directory where CI job traces are stored
sudo chmod -R u+rwX builds/
# Change the permissions of the directory where CI artifacts are stored
sudo chmod -R u+rwX shared/artifacts/
+ # Change the permissions of the directory where GitLab Pages are stored
+ sudo chmod -R ug+rwX shared/pages/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -448,7 +468,8 @@ Check if GitLab and its environment are configured correctly:
### Compile Assets
- sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
+ sudo -u git -H yarn install --production --pure-lockfile
+ sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
### Start Your GitLab Instance
@@ -456,7 +477,7 @@ Check if GitLab and its environment are configured correctly:
# or
sudo /etc/init.d/gitlab restart
-## 8. Nginx
+## 9. Nginx
**Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/).
@@ -484,6 +505,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat
# or else sudo rm -f /etc/nginx/sites-enabled/default
sudo editor /etc/nginx/sites-available/gitlab
+If you intend to enable GitLab pages, there is a separate Nginx config you need
+to use. Read all about the needed configuration at the
+[GitLab Pages administration guide](../administration/pages/index.md).
+
**Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details.
### Test Configuration
@@ -633,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 e942346e2d7..3f90597ec80 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -15,11 +15,11 @@ For the installations options please see [the installation page on the GitLab we
### Unsupported Unix distributions
-- OS X
- Arch Linux
- Fedora
-- Gentoo
- FreeBSD
+- Gentoo
+- macOS
On the above unsupported distributions is still possible to install GitLab yourself.
Please see the [installation from source guide](installation.md) and the [installation guides](https://about.gitlab.com/installation/) for more information.
@@ -120,7 +120,12 @@ To change the Unicorn workers when you have the Omnibus package please see [the
## Database
-If you want to run the database separately expect a size of about 1 MB per user.
+We currently support the following databases:
+
+- PostgreSQL (recommended)
+- MySQL/MariaDB
+
+If you want to run the database separately, expect a size of about 1 MB per user.
### PostgreSQL Requirements
@@ -128,7 +133,9 @@ Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
by running the following query for every database:
- CREATE EXTENSION pg_trgm;
+```
+CREATE EXTENSION pg_trgm;
+```
On some systems you may need to install an additional package (e.g.
`postgresql-contrib`) for this extension to become available.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index e97430feb57..e56e58498a6 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,30 +5,28 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker
+- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
- [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
- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
-GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
-
-[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
-
+> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
## Project services
Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
Pivotal Tracker, and Slack are available in the form of a [Project Service][].
-[Project Service]: ../project_services/project_services.md
+[Project Service]: ../user/project/integrations/project_services.md
## SSL certificate errors
@@ -64,3 +62,5 @@ After that restart GitLab with:
```bash
sudo gitlab-ctl restart
```
+
+[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index e5247082a89..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'
}
@@ -80,10 +80,13 @@ from step 5.
1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
page from step 5.
-1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
-for the changes to take effect.
+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 an Auth0 icon below the regular sign in
form. Click the icon to begin the authentication process. Auth0 will ask the
user to sign in and authorize the GitLab application. If everything goes well
the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 48dddf7df44..5e3e9f5ab77 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -78,6 +78,10 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/cas.md b/doc/integration/cas.md
index e34e306f9ac..f757edf0bc2 100644
--- a/doc/integration/cas.md
+++ b/doc/integration/cas.md
@@ -58,8 +58,11 @@ To enable the CAS OmniAuth provider you must register your application with your
1. Save the configuration file.
-1. Run `gitlab-ctl reconfigure` for the omnibus package.
-
-1. Restart GitLab for the changes to take effect.
+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 CAS tab in the sign in form.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index 40d93aef2a9..2bc526dc3db 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -1,58 +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. Restart GitLab for the changes to take effect.
-
-On the sign in page there should now be a Crowd tab in the sign in form. \ No newline at end of file
+This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md).
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 8d2c6351fb8..265c891cf83 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -18,9 +18,9 @@ The configuration is done via a project's **Services**.
To enable an external issue tracker you must configure the appropriate **Service**.
Visit the links below for details:
-- [Redmine](../project_services/redmine.md)
-- [Jira](../project_services/jira.md)
-- [Bugzilla](../project_services/bugzilla.md)
+- [Redmine](../user/project/integrations/redmine.md)
+- [Jira](../user/project/integrations/jira.md)
+- [Bugzilla](../user/project/integrations/bugzilla.md)
### Service Template
@@ -28,4 +28,4 @@ To save you the hassle from configuring each project's service individually,
GitLab provides the ability to set Service Templates which can then be
overridden in each project's settings.
-Read more on [Services Templates](../project_services/services_templates.md).
+Read more on [Services Templates](../user/project/integrations/services_templates.md).
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
index 77bb75cbfca..a67de23b17b 100644
--- a/doc/integration/facebook.md
+++ b/doc/integration/facebook.md
@@ -92,6 +92,10 @@ something else descriptive.
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 479c697b933..4b0d33334bd 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -2,7 +2,7 @@
Import projects from GitHub and login to your GitLab instance with your GitHub account.
-To enable the GitHub OmniAuth provider you must register your application with GitHub.
+To enable the GitHub OmniAuth provider you must register your application with GitHub.
GitHub will generate an application ID and secret key for you to use.
1. Sign in to GitHub.
@@ -19,10 +19,10 @@ 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).
+1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration.
![GitHub app](img/github_app.png)
@@ -49,7 +49,7 @@ GitHub will generate an application ID and secret key for you to use.
For omnibus package:
For GitHub.com:
-
+
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -60,9 +60,9 @@ GitHub will generate an application ID and secret key for you to use.
}
]
```
-
+
For GitHub Enterprise:
-
+
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -101,10 +101,14 @@ GitHub will generate an application ID and secret key for you to use.
1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7.
-1. Save the configuration file and run `sudo gitlab-ctl reconfigure`.
+1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 GitHub icon below the regular sign in form.
-Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
+On the sign in page there should now be a GitHub icon below the regular sign in form.
+Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 6d8f3912ede..eec40a9b8f1 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -2,7 +2,7 @@
Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account.
-To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
+To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
GitLab.com will generate an application ID and secret key for you to use.
1. Sign in to GitLab.com
@@ -26,8 +26,8 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select "Submit".
-1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
- Keep this page open as you continue configuration.
+1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
+ Keep this page open as you continue configuration.
![GitLab app](img/gitlab_app.png)
1. On your GitLab server, open the configuration file.
@@ -77,8 +77,12 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 GitLab.com icon below the regular sign in form.
-Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
+On the sign in page there should now be a GitLab.com icon below the regular sign in form.
+Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to your GitLab instance and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 82978b68a34..1e7ad90c5a8 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -74,7 +74,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
@@ -87,3 +88,6 @@ At this point, when users first try to authenticate to your GitLab installation
1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window).
1. Scroll down until you find "Product Name". Change the product name to something more descriptive.
1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index e2f136bcc35..b6923f74e28 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,3 +1 @@
-# GitLab JIRA integration
-
-This document was moved to [project_services/jira](../project_services/jira.md).
+This document was moved to [integrations/jira](../user/project/integrations/jira.md).
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 30f0c15dacc..242890af981 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -1,3 +1 @@
-# GitLab LDAP integration
-
-This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
+This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md).
diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md
new file mode 100644
index 00000000000..e71706fef7d
--- /dev/null
+++ b/doc/integration/oauth2_generic.md
@@ -0,0 +1,65 @@
+# Sign into GitLab with (almost) any OAuth2 provider
+
+The `omniauth-oauth2-generic` gem allows Single Sign On between GitLab and your own OAuth2 provider
+(or any OAuth2 provider compatible with this gem)
+
+This strategy is designed to allow configuration of the simple OmniAuth SSO process outlined below:
+
+1. Strategy directs client to your authorization URL (**configurable**), with specified ID and key
+1. OAuth provider handles authentication of request, user, and (optionally) authorization to access user's profile
+1. OAuth provider directs client back to GitLab where Strategy handles retrieval of access token
+1. Strategy requests user information from a **configurable** "user profile" URL (using the access token)
+1. Strategy parses user information from the response, using a **configurable** format
+1. GitLab finds or creates the returned user and logs them in
+
+### Limitations of this Strategy:
+
+- It can only be used for Single Sign on, and will not provide any other access granted by any OAuth provider
+ (importing projects or users, etc)
+- It only supports the Authorization Grant flow (most common for client-server applications, like GitLab)
+- It is not able to fetch user information from more than one URL
+- It has not been tested with user information formats other than JSON
+
+### Config Instructions
+
+1. Register your application in the OAuth2 provider you wish to authenticate with.
+
+ The redirect URI you provide when registering the application should be:
+
+ ```
+ http://your-gitlab.host.com/users/auth/oauth2_generic/callback
+ ```
+
+1. You should now be able to get a Client ID and Client Secret.
+ Where this shows up will differ for each provider.
+ This may also be called Application ID and Secret
+
+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-specific configuration for your provider, as [described in the gem's README][1]
+
+1. Save the configuration file
+
+1. Restart GitLab for the changes to take effect
+
+On the sign in page there should now be a new button below the regular sign in form.
+Click the button to begin your provider's authentication process. This will direct
+the browser to your OAuth2 Provider's authentication page. If everything goes well
+the user will be returned to your GitLab instance and will be signed in.
+
+[1]: https://gitlab.com/satorix/omniauth-oauth2-generic#gitlab-config-example \ No newline at end of file
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 98a680d0dbe..6c11f46a70a 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -27,10 +27,11 @@ 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)
+- [OAuth2Generic](oauth2_generic.md)
## Initial OmniAuth Configuration
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 4a242c321aa..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"
}
@@ -109,7 +109,8 @@ in your SAML IdP:
1. Change the value of `issuer` to a unique name, which will identify the application
to the IdP.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
in `issuer`.
@@ -171,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'
} }
```
@@ -226,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'] }
}
```
@@ -244,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
}
@@ -314,3 +315,6 @@ For this you need take the following into account:
Make sure that one of the above described scenarios is valid, or the requests will
fail with one of the mentioned errors.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 696c1011eeb..e0fc1bb801f 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -70,10 +70,9 @@ gitlab_rails['omniauth_providers'] = [
]
```
-1. Save changes and reconfigure gitlab:
-```
-sudo gitlab-ctl reconfigure
-```
+
+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 "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in.
@@ -122,4 +121,7 @@ you will not get a shibboleth session!
RequestHeader set X_FORWARDED_PROTO 'https'
RequestHeader set X-Forwarded-Ssl on
-``` \ No newline at end of file
+```
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index abbea09f22f..d0976b6201e 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -74,6 +74,10 @@ To enable the Twitter OmniAuth provider you must register your application with
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+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 Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/pages/README.md b/doc/pages/README.md
new file mode 100644
index 00000000000..7878bce3f10
--- /dev/null
+++ b/doc/pages/README.md
@@ -0,0 +1 @@
+This document was moved to [pages/index.md](../user/project/pages/index.md).
diff --git a/doc/pages/administration.md b/doc/pages/administration.md
new file mode 100644
index 00000000000..4eb3bb32c77
--- /dev/null
+++ b/doc/pages/administration.md
@@ -0,0 +1 @@
+This document was moved to [administration/pages](../administration/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/profile/preferences.md b/doc/profile/preferences.md
index 073b8797508..4f2b00f3dd1 100644
--- a/doc/profile/preferences.md
+++ b/doc/profile/preferences.md
@@ -3,13 +3,6 @@
Settings in the **Profile > Preferences** page allow the user to customize
various aspects of the site to their liking.
-## Application theme
-
-Changing this setting allows the user to customize the color scheme used for the
-navigation bar on the left side of the screen.
-
-The default is **Charcoal**.
-
## Syntax highlighting theme
_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md
index 51668128c62..5b171080c72 100644
--- a/doc/project_services/bamboo.md
+++ b/doc/project_services/bamboo.md
@@ -1,60 +1 @@
-# Atlassian Bamboo CI Service
-
-GitLab provides integration with Atlassian Bamboo for continuous integration.
-When configured, pushes to a project will trigger a build in Bamboo automatically.
-Merge requests will also display CI status showing whether the build is pending,
-failed, or completed successfully. It also provides a link to the Bamboo build
-page for more information.
-
-Bamboo doesn't quite provide the same features as a traditional build system when
-it comes to accepting webhooks and commit data. There are a few things that
-need to be configured in a Bamboo build plan before GitLab can integrate.
-
-## Setup
-
-### Complete these steps in Bamboo:
-
-1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
-dropdown.
-1. Select the 'Triggers' tab.
-1. Click 'Add trigger'.
-1. Enter a description such as 'GitLab trigger'
-1. Choose 'Repository triggers the build when changes are committed'
-1. Check one or more repositories checkboxes
-1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
-whitelist of IP addresses that are allowed to trigger Bamboo builds.
-1. Save the trigger.
-1. In the left pane, select a build stage. If you have multiple build stages
-you want to select the last stage that contains the git checkout task.
-1. Select the 'Miscellaneous' tab.
-1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
-in the 'Labels' box.
-1. Save
-
-Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
-service in GitLab
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure to trigger builds.
-1. Select 'Settings' in the top navigation.
-1. Select 'Services' in the left navigation.
-1. Click 'Atlassian Bamboo CI'
-1. Select the 'Active' checkbox.
-1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
-all capital letter, identifier that is unique. It will be something like PR-BLD
-1. If necessary, enter username and password for a Bamboo user that has
-access to trigger the build plan. Leave these fields blank if you do not require
-authentication.
-1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
-will actually trigger a build in Bamboo.
-
-## Troubleshooting
-
-If builds are not triggered, these are a couple of things to keep in mind.
-
-1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
-IP addresses'.
-1. Remember that GitLab only triggers builds on push events. A commit via the
-web interface will not trigger CI currently.
+This document was moved to [user/project/integrations/bamboo.md](../user/project/integrations/bamboo.md).
diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md
index 215ed6fe9cc..e67055d5616 100644
--- a/doc/project_services/bugzilla.md
+++ b/doc/project_services/bugzilla.md
@@ -1,17 +1 @@
-# Bugzilla Service
-
-Go to your project's **Settings > Services > Bugzilla** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-
-Once you have configured and enabled Bugzilla:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Bugzilla product page
-- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
+This document was moved to [user/project/integrations/bugzilla.md](../user/project/integrations/bugzilla.md).
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
index af0b1a287c7..ee54d865225 100644
--- a/doc/project_services/builds_emails.md
+++ b/doc/project_services/builds_emails.md
@@ -1,16 +1 @@
-## Enabling build emails
-
-To receive e-mail notifications about the result status of your builds, visit
-your project's **Settings > Services > Builds emails** and activate the service.
-
-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.
-
----
-
-![Builds emails service settings](img/builds_emails_service.png)
+This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
index 2f9f36f962e..a2e831ada34 100644
--- a/doc/project_services/emails_on_push.md
+++ b/doc/project_services/emails_on_push.md
@@ -1,17 +1 @@
-## Enabling emails on push
-
-To receive email notifications for every change that is pushed to the project, visit
-your project's **Settings > Services > Emails on push** and activate the service.
-
-In the _Recipients_ area, provide a list of emails separated by commas.
-
-You can configure any of the following settings depending on your preference.
-
-+ **Push events** - Email will be triggered when a push event is recieved
-+ **Tag push events** - Email will be triggered when a tag is created and pushed
-+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
-+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
-
----
-
-![Email on push service settings](img/emails_on_push_service.png)
+This document was moved to [user/project/integrations/emails_on_push.md](../user/project/integrations/emails_on_push.md).
diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md
index 021a93a288f..4ae9f6c6b2e 100644
--- a/doc/project_services/hipchat.md
+++ b/doc/project_services/hipchat.md
@@ -1,54 +1 @@
-# Atlassian HipChat
-
-GitLab provides a way to send HipChat notifications upon a number of events,
-such as when a user pushes code, creates a branch or tag, adds a comment, and
-creates a merge request.
-
-## Setup
-
-GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
-not supported at this time. Note the differences between v1 and v2 tokens:
-
-HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
-token is allowed to send messages to *any* room.
-
-HipChat v2 API has tokens that are can be created using the Integrations tab
-in the Group or Room admin page. By design, these are lightweight tokens that
-allow GitLab to send messages only to *one* room.
-
-### Complete these steps in HipChat:
-
-1. Go to: https://admin.hipchat.com/admin
-1. Click on "Group Admin" -> "Integrations".
-1. Find "Build Your Own!" and click "Create".
-1. Select the desired room, name the integration "GitLab", and click "Create".
-1. In the "Send messages to this room by posting this URL" column, you should
-see a URL in the format:
-
-```
- https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
-```
-
-HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
-service in GitLab.
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "HipChat".
-1. Select the "Active" checkbox.
-1. Insert the `token` field from the URL into the `Token` field on the Web page.
-1. Insert the `room` field from the URL into the `Room` field on the Web page.
-1. Save or optionally click "Test Settings".
-
-## Troubleshooting
-
-If you do not see notifications, make sure you are using a HipChat v2 API
-token, not a v1 token.
-
-Note that the v2 token is tied to a specific room. If you want to be able to
-specify arbitrary rooms, you can create an API token for a specific user in
-HipChat under "Account settings" and "API access". Use the `XXX` value under
-`auth_token=XXX`.
+This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md).
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
deleted file mode 100644
index 9dbbed03833..00000000000
--- a/doc/project_services/img/builds_emails_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png
deleted file mode 100644
index a62e4b792f9..00000000000
--- a/doc/project_services/img/mattermost_config_help.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png
deleted file mode 100644
index 3c5ff5ee317..00000000000
--- a/doc/project_services/img/mattermost_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png
deleted file mode 100644
index 50d20510daf..00000000000
--- a/doc/project_services/img/services_templates_redmine_example.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png
deleted file mode 100644
index fc8e58e686b..00000000000
--- a/doc/project_services/img/slack_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/slack_setup.png b/doc/project_services/img/slack_setup.png
deleted file mode 100644
index f69817f2b78..00000000000
--- a/doc/project_services/img/slack_setup.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md
index 25c0c3ad2a6..7f0850dcc24 100644
--- a/doc/project_services/irker.md
+++ b/doc/project_services/irker.md
@@ -1,51 +1 @@
-# Irker IRC Gateway
-
-GitLab provides a way to push update messages to an Irker server. When
-configured, pushes to a project will trigger the service to send data directly
-to the Irker server.
-
-See the project homepage for further info: https://gitlab.com/esr/irker
-
-## Needed setup
-
-You will first need an Irker daemon. You can download the Irker code from its
-repository on https://gitlab.com/esr/irker:
-
-```
-git clone https://gitlab.com/esr/irker.git
-```
-
-Once you have downloaded the code, you can run the python script named `irkerd`.
-This script is the gateway script, it acts both as an IRC client, for sending
-messages to an IRC server obviously, and as a TCP server, for receiving messages
-from the GitLab service.
-
-If the Irker server runs on the same machine, you are done. If not, you will
-need to follow the firsts steps of the next section.
-
-## Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "Irker".
-1. Select the "Active" checkbox.
-1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
-in the `Server host` field on the Web page
-1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
-`Server port` field on the Web page.
-1. Optional: if `Default IRC URI` is set, it has to be in the format
-`irc[s]://domain.name` and will be prepend to each and every channel provided
-by the user which is not a full URI.
-1. Specify the recipients (e.g. #channel1, user1, etc.)
-1. Save or optionally click "Test Settings".
-
-## Note on Irker recipients
-
-Irker accepts channel names of the form `chan` and `#chan`, both for the
-`#chan` channel. If you want to send messages in query, you will need to add
-`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
-case, `Aorimn` is treated as a nick and no more as a channel name.
-
-Irker can also join password-protected channels. Users need to append
-`?key=thesecretpassword` to the chan name.
+This document was moved to [user/project/integrations/irker.md](../user/project/integrations/irker.md).
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 390066c9989..63614feba82 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1,208 +1 @@
-# GitLab JIRA integration
-
-GitLab can be configured to interact with JIRA. Configuration happens via
-user name and password. Connecting to a JIRA server via CAS is not possible.
-
-Each project can be configured to connect to a different JIRA instance, see the
-[configuration](#configuration) section. If you have one JIRA instance you can
-pre-fill the settings page with a default template. To configure the template
-see the [Services Templates][services-templates] document.
-
-Once the project is connected to JIRA, you can reference and close the issues
-in JIRA directly from GitLab.
-
-## Configuration
-
-In order to enable the JIRA service in GitLab, you need to first configure the
-project in JIRA and then enter the correct values in GitLab.
-
-### Configuring JIRA
-
-We need to create a user in JIRA which will have access to all projects that
-need to integrate with GitLab. Login to your JIRA instance as admin and under
-Administration go to User Management and create a new user.
-
-As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
-group.
-
-**It is important that the user `GitLab` has write-access to projects in JIRA**
-
-We have split this stage in steps so it is easier to follow.
-
----
-
-1. Login to your JIRA instance as an administrator and under **Administration**
- go to **User Management** to create a new user.
-
- ![JIRA user management link](img/jira_user_management_link.png)
-
- ---
-
-1. The next step is to create a new user (e.g., `gitlab`) who has write access
- to projects in JIRA. Enter the user's name and a _valid_ e-mail address
- since JIRA sends a verification e-mail to set-up the password.
- _**Note:** JIRA creates the username automatically by using the e-mail
- prefix. You can change it later if you want._
-
- ![JIRA create new user](img/jira_create_new_user.png)
-
- ---
-
-1. Now, let's create a `gitlab-developers` group which will have write access
- to projects in JIRA. Go to the **Groups** tab and select **Create group**.
-
- ![JIRA create new user](img/jira_create_new_group.png)
-
- ---
-
- Give it an optional description and hit **Create group**.
-
- ![jira create new group](img/jira_create_new_group_name.png)
-
- ---
-
-1. Give the newly-created group write access by going to
- **Application access ➔ View configuration** and adding the `gitlab-developers`
- group to JIRA Core.
-
- ![JIRA group access](img/jira_group_access.png)
-
- ---
-
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_ which is
- what we aim for.
-
- ![JIRA add user to group](img/jira_add_user_to_group.png)
-
----
-
-The JIRA configuration is over. Write down the new JIRA username and its
-password as they will be needed when configuring GitLab in the next section.
-
-### Configuring GitLab
-
->**Notes:**
-- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
- higher is required.
-- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
- the configuration options you have to enter. If you are using an older version,
- [follow this documentation][jira-repo-docs].
-
-To enable JIRA integration in a project, navigate to your project's
-**Services ➔ JIRA** and fill in the required details on the page as described
-in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
-| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
-| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
-
-After saving the configuration, your GitLab project will be able to interact
-with the linked JIRA project.
-
-![JIRA service page](img/jira_service_page.png)
-
----
-
-## JIRA issues
-
-By now you should have [configured JIRA](#configuring-jira) and enabled the
-[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
-you should be able to reference and close JIRA issues by just mentioning their
-ID in GitLab commits and merge requests.
-
-### Referencing JIRA Issues
-
-When GitLab project has JIRA issue tracker configured and enabled, mentioning
-JIRA issue in GitLab will automatically add a comment in JIRA issue with the
-link back to GitLab. This means that in comments in merge requests and commits
-referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the
-format:
-
-```
-USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
-ENTITY_TITLE
-```
-
-* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
-* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
-* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
-* `PROJECT_NAME` GitLab project name.
-* `ENTITY_TITLE` Merge request title or commit message first line.
-
-![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
-
----
-
-### Closing JIRA Issues
-
-JIRA issues can be closed directly from GitLab by using trigger words in
-commits and merge requests. When a commit which contains the trigger word
-followed by the JIRA issue ID in the commit message is pushed, GitLab will
-add a comment in the mentioned JIRA issue and immediately close it (provided
-the transition ID was set up correctly).
-
-There are currently three trigger words, and you can use either one to achieve
-the same goal:
-
-- `Resolves PROJECT-1`
-- `Closes PROJECT-1`
-- `Fixes PROJECT-1`
-
-where `PROJECT-1` is the issue ID of the JIRA project.
-
-### JIRA issue closing example
-
-Let's consider the following example:
-
-1. For the project named `PROJECT` in JIRA, we implemented a new feature
- and created a merge request in GitLab.
-1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
- in GitLab contains the improvement
-1. In the merge request description we use the issue closing trigger
- `Closes PROJECT-7`.
-1. Once the merge request is merged, the JIRA issue will be automatically closed
- with a comment and an associated link to the commit that resolved the issue.
-
----
-
-In the following screenshot you can see what the link references to the JIRA
-issue look like.
-
-![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
-
----
-
-Once this merge request is merged, the JIRA issue will be automatically closed
-with a link to the commit that resolved the issue.
-
-![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png)
-
----
-
-![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
-
-## Troubleshooting
-
-If things don't work as expected that's usually because you have configured
-incorrectly the JIRA-GitLab integration.
-
-### GitLab is unable to comment on a ticket
-
-Make sure that the user you set up for GitLab to communicate with JIRA has the
-correct access permission to post comments on a ticket and to also transition
-the ticket, if you'd like GitLab to also take care of closing them.
-JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
-
-### GitLab is unable to close a ticket
-
-Make sure the `Transition ID` you set within the JIRA settings matches the one
-your project needs to close a ticket.
-
-[services-templates]: ../project_services/services_templates.md
-[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
+This document was moved to [user/project/integrations/jira.md](../user/project/integrations/jira.md).
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
index 99aa9e44bdb..0497a13c2b7 100644
--- a/doc/project_services/kubernetes.md
+++ b/doc/project_services/kubernetes.md
@@ -1,63 +1 @@
-# GitLab Kubernetes / OpenShift integration
-
-GitLab can be configured to interact with Kubernetes, or other systems using the
-Kubernetes API (such as OpenShift).
-
-Each project can be configured to connect to a different Kubernetes cluster, see
-the [configuration](#configuration) section.
-
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see the [Services Templates](services_templates.md) document.
-
-## Configuration
-
-![Kubernetes configuration settings](img/kubernetes_configuration.png)
-
-The Kubernetes service takes the following arguments:
-
-1. Kubernetes namespace
-1. API URL
-1. Service token
-1. Custom CA bundle
-
-The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
-exposes several APIs - we want the "base" URL that is common to all of them,
-e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-
-GitLab authenticates against Kubernetes using service tokens, which are
-scoped to a particular `namespace`. If you don't have a service token yet,
-you can follow the
-[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
-to create one. You can also view or create service tokens in the
-[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
-`Config -> Secrets`.
-
-Fill in the service token and namespace according to the values you just got.
-If the API is using a self-signed TLS certificate, you'll also need to include
-the `ca.crt` contents as the `Custom CA bundle`.
-
-## Deployment variables
-
-The Kubernetes service exposes following
-[deployment variables](../ci/variables/README.md#deployment-variables) in the
-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
-
-## Web terminals
-
->**NOTE:**
-Added in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is currently limited to the first container in the
-first pod of your environment.
-
-When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals)
-support to your environments. This is based on the `exec` functionality found in
-Docker and Kubernetes, so you get a new shell session within your existing
-containers. To use this integration, you should deploy to Kubernetes using
-the deployment variables above, ensuring any pods you create are labelled with
-`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+This document was moved to [user/project/integrations/kubernetes.md](../user/project/integrations/kubernetes.md).
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
index fbc7dfeee6d..554a028853e 100644
--- a/doc/project_services/mattermost.md
+++ b/doc/project_services/mattermost.md
@@ -1,45 +1 @@
-# Mattermost Notifications Service
-
-## On Mattermost
-
-To enable Mattermost integration you must create an incoming webhook integration:
-
-1. Sign in to your Mattermost instance
-1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
-1. Choose a display name, description and channel, those can be overridden on GitLab
-1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
-
-There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
-it on https://mattermost.example/admin_console/integrations/custom.
-
-Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
-
-## On GitLab
-
-After you set up Mattermost, it's time to set up GitLab.
-
-Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- 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.
-
-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... |
-| **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. |
-
-
-![Mattermost configuration](img/mattermost_configuration.png)
+This document was moved to [user/project/integrations/mattermost.md](../user/project/integrations/mattermost.md).
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 67cb88104c1..7c238b5dc37 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -1,163 +1 @@
-# Mattermost slash commands
-
-> Introduced in GitLab 8.14
-
-Mattermost commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in Mattermost.
-
-## Prerequisites
-
-Mattermost 3.4 and up is required.
-
-If you have the Omnibus GitLab package installed, Mattermost is already bundled
-in it. All you have to do is configure it. Read more in the
-[Omnibus GitLab Mattermost documentation][omnimmdocs].
-
-## Automated Configuration
-
-If Mattermost is installed on the same server as GitLab, the configuration process can be
-done for you by GitLab.
-
-Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
-
-## Manual Configuration
-
-The configuration consists of two parts. First you need to enable the slash
-commands in Mattermost and then enable the service in GitLab.
-
-### Step 1. Enable custom slash commands in Mattermost
-
-This step is only required when using a source install, omnibus installs will be
-preconfigured with the right settings.
-
-The first thing to do in Mattermost is to enable custom slash commands from
-the administrator console.
-
-1. Log in with an account that has admin privileges and navigate to the system
- console.
-
- ![Mattermost go to console](img/mattermost_goto_console.png)
-
- ---
-
-1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
- **Enable custom integrations to override usernames**, and **Override
- custom integrations to override profile picture icons** to true
-
- ![Mattermost console](img/mattermost_console_integrations.png)
-
- ---
-
-1. Click **Save** at the bottom to save the changes.
-
-### Step 2. Open the Mattermost slash commands service in GitLab
-
-1. Open a new tab for GitLab and go to your project's settings
- **Services ➔ Mattermost command**. A screen will appear with all the values you
- need to copy in Mattermost as described in the next step. Leave the window open.
-
- >**Note:**
- GitLab will propose some values for the Mattermost settings. The only one
- required to copy-paste as-is is the **Request URL**, all the others are just
- suggestions.
-
- ![Mattermost setup instructions](img/mattermost_config_help.png)
-
- ---
-
-1. Proceed to the next step and create a slash command in Mattermost with the
- above values.
-
-### Step 3. Create a new custom slash command in Mattermost
-
-Now that you have enabled custom slash commands in Mattermost and opened
-the Mattermost slash commands service in GitLab, it's time to copy these values
-in a new slash command.
-
-1. Back to Mattermost, under your team page settings, you should see the
- **Integrations** option.
-
- ![Mattermost team integrations](img/mattermost_team_integrations.png)
-
- ---
-
-1. Go to the **Slash Commands** integration and add a new one by clicking the
- **Add Slash Command** button.
-
- ![Mattermost add command](img/mattermost_add_slash_command.png)
-
- ---
-
-1. Fill in the options for the custom command as described in
- [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
-
- >**Note:**
- If you plan on connecting multiple projects, pick a slash command trigger
- word that relates to your projects such as `/gitlab-project-name` or even
- just `/project-name`. Only use `/gitlab` if you will only connect a single
- project to your Mattermost team.
-
- ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
-
-1. After you setup all the values, copy the token (we will use it below) and
- click **Done**.
-
- ![Mattermost slash command token](img/mattermost_slash_command_token.png)
-
-### Step 4. Copy the Mattermost token into the Mattermost slash command service
-
-1. In GitLab, paste the Mattermost token you copied in the previous step and
- check the **Active** checkbox.
-
- ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
-
-1. Click **Save changes** for the changes to take effect.
-
----
-
-You are now set to start using slash commands in Mattermost that talk to the
-GitLab project you configured.
-
-## Authorizing Mattermost to interact with GitLab
-
-The first time a user will interact with the newly created slash commands,
-Mattermost will trigger an authorization process.
-
-![Mattermost bot authorize](img/mattermost_bot_auth.png)
-
-This will connect your Mattermost user with your GitLab user. You can
-see all authorized chat accounts in your profile's page under **Chat**.
-
-When the authorization process is complete, you can start interacting with
-GitLab using the Mattermost commands.
-
-## Available slash commands
-
-The available slash commands are:
-
-| Command | Description | Example |
-| ------- | ----------- | ------- |
-| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
-| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
-| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-
-To see a list of available commands to interact with GitLab, type the
-trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
-
-![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
-
-## Permissions
-
-The permissions to run the [available commands](#available-commands) derive from
-the [permissions you have on the project](../user/permissions.md#project).
-
-## Further reading
-
-- [Mattermost slash commands documentation][mmslashdocs]
-- [Omnibus GitLab Mattermost][omnimmdocs]
-
-
-[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
-[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
-[ciyaml]: ../ci/yaml/README.md
+This document was moved to [user/project/integrations/mattermost_slash_commands.md](../user/project/integrations/mattermost_slash_commands.md).
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 547d855d777..2c555c4edae 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -1,59 +1 @@
-# Project Services
-
-Project services allow you to integrate GitLab with other applications. Below
-is list of the currently supported ones.
-
-You can find these within GitLab in the Services page under Project Settings if
-you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in
-adding functionality to GitLab. For example there is also a service that can
-send an email every time someone pushes new commits.
-
-Because GitLab is open source we can ship with the code and tests for all
-plugins. This allows the community to keep the plugins up to date so that they
-always work in newer GitLab versions.
-
-For an overview of what projects services are available without logging in,
-please see the [project_services directory][projects-code].
-
-[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
-
-Click on the service links to see
-further configuration instructions and details. Contributions are welcome.
-
-## Services
-
-| Service | Description |
-| ------- | ----------- |
-| Asana | Asana - Teamwork without email |
-| 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 |
-| Drone CI | Continuous Integration platform built on Docker, written in Go |
-| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
-| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
-| Flowdock | Flowdock is a collaboration web app for technical teams |
-| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
-| [HipChat](hipchat.md) | Private group chat and IM |
-| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
-| [JIRA](jira.md) | JIRA issue tracker |
-| JetBrains TeamCity CI | A continuous integration and build server |
-| [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 |
-| [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) |
-| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
-| [Redmine](redmine.md) | Redmine issue tracker |
-
-## Services Templates
-
-Services templates is a way to set some predefined values in the Service of
-your liking which will then be pre-filled on each project's Service.
-
-Read more about [Services Templates in this document](services_templates.md).
+This document was moved to [user/project/integrations/project_services.md](../user/project/integrations/project_services.md).
diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md
index b9830ea7c38..6010aa4dc75 100644
--- a/doc/project_services/redmine.md
+++ b/doc/project_services/redmine.md
@@ -1,21 +1 @@
-# Redmine Service
-
-Go to your project's **Settings > Services > Redmine** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
-| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
-
-Once you have configured and enabled Redmine:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Redmine issue index
-- clicking **New issue** on the project dashboard creates a new Redmine issue
-
-As an example, below is a configuration for a project named gitlab-ci.
-
-![Redmine configuration](img/redmine_configuration.png)
+This document was moved to [user/project/integrations/redmine.md](../user/project/integrations/redmine.md).
diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md
index be6d13b6d2b..8905d667c5a 100644
--- a/doc/project_services/services_templates.md
+++ b/doc/project_services/services_templates.md
@@ -1,25 +1 @@
-# Services Templates
-
-A GitLab administrator can add a service template that sets a default for each
-project. This makes it much easier to configure individual projects.
-
-After the template is created, the template details will be pre-filled on a
-project's Service page.
-
-## Enable a Service template
-
-In GitLab's Admin area, navigate to **Service Templates** and choose the
-service template you wish to create.
-
-For example, in the image below you can see Redmine.
-
-![Redmine service template](img/services_templates_redmine_example.png)
-
----
-
-**NOTE:** For each project, you will still need to configure the issue tracking
-URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
-by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
-the project settings, and GitLab would automatically update the URL configured
-in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
-must be configured directly within the project's **Services** settings.
+This document was moved to [user/project/integrations/services_templates.md](../user/project/integrations/services_templates.md).
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index eaceb2be137..1d3f98705e3 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,50 +1 @@
-# Slack Notifications Service
-
-## On Slack
-
-To enable Slack integration you must create an incoming webhook integration on
-Slack:
-
-1. [Sign in to Slack](https://slack.com/signin)
-1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-1. Choose the channel name you want to send notifications to.
-1. Click **Add Incoming WebHooks Integration**
-1. Copy the **Webhook URL**, we'll need this later for GitLab.
-
-## On GitLab
-
-After you set up Slack, it's time to set up GitLab.
-
-Go to your project's **Settings > Integrations > Slack Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- 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 (`#`).
-
-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. |
-
-After you are all done, click **Save changes** for the changes to take effect.
-
->**Note:**
-You can set "branch,pushed,Compare changes" as highlight words on your Slack
-profile settings, so that you can be aware of new commits when somebody pushes
-them.
-
-![Slack configuration](img/slack_configuration.png)
-
-[slackhook]: https://my.slack.com/services/new/incoming-webhook
+This document was moved to [user/project/integrations/slack.md](../user/project/integrations/slack.md).
diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md
index d9ff573d185..9554c8decc8 100644
--- a/doc/project_services/slack_slash_commands.md
+++ b/doc/project_services/slack_slash_commands.md
@@ -1,23 +1 @@
-# Slack slash commands
-
-> Introduced in GitLab 8.15
-
-Slack commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in chat.
-For all available commands try the help subcommand, for example: `/gitlab help`,
-all review the [full list of commands](../integration/chat_commands.md).
-
-## Prerequisites
-
-A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you.
-
-## Configuration
-
-First, navigate to the Slack Slash commands service page, found at your project's
-**Settings** > **Services**, and you find the instructions there:
-
- ![Slack setup instructions](img/slack_setup.png)
-
-Once you've followed the instructions, mark the service as active and insert the token
-you've received from Slack. After saving the service you are good to go!
+This document was moved to [user/project/integrations/slack_slash_commands.md](../user/project/integrations/slack_slash_commands.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6b4db71b44..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 build output logs)
-- `artifacts` (CI build 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:
```
@@ -84,6 +67,52 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Backup Strategy Option
+
+> **Note:** Introduced as an option in 8.17
+
+The default backup strategy is to essentially stream data from the respective
+data locations to the backup using the Linux command `tar` and `gzip`. This works
+fine in most cases, but can cause problems when data is rapidly changing.
+
+When data changes while `tar` is reading it, the error `file changed as we read
+it` may occur, and will cause the backup process to fail. To combat this, 8.17
+introduces a new backup strategy called `copy`. The strategy copies data files
+to a temporary location before calling `tar` and `gzip`, avoiding the error.
+
+A side-effect is that the backup process with take up to an additional 1X disk
+space. The process does its best to clean up the temporary files at each stage
+so the problem doesn't compound, but it could be a considerable change for large
+installations. This is why the `copy` strategy is not the default in 8.17.
+
+To use the `copy` strategy instead of the default streaming strategy, specify
+`STRATEGY=copy` in the Rake task command. For example,
+`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
+
+## Exclude specific directories from the backup
+
+You can choose what should be backed up by adding the environment variable `SKIP`.
+The available options are:
+
+- `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:
+
+```
+# use this command if you've installed GitLab with the Omnibus package
+sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+
+# if you've installed GitLab from source
+sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
+```
+
## Upload backups to remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
@@ -130,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
@@ -371,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/raketasks/features.md b/doc/raketasks/features.md
index f9a46193547..fee49cc27cc 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -7,7 +7,7 @@ This command will enable the namespaces feature introduced in v4.0. It will move
Note:
- Because the **repository location will change**, you will need to **update all your git URLs** to point to the new location.
-- Username can be changed at [Profile / Account](/profile/account)
+- Username can be changed at **Profile ➔ Account**.
**Example:**
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index bb46aebf4b5..faabc53ce72 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,7 +2,7 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
@@ -10,4 +10,4 @@ Because Webhook requests are made by the GitLab server itself, these have comple
If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file
+To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9e391d647a8..678f5199b02 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,8 +162,8 @@ 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
@@ -150,7 +172,8 @@ 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.
+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.
If you want to add the same key to another project, please enable it in the
@@ -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/university/README.md b/doc/university/README.md
index 12727e9d56f..c1661f0b52b 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/).
-Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information.
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) for more information.
## Gitlab University Curriculum
@@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
-1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
+1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/)
#### 2.2. GitLab Issues
@@ -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
@@ -189,10 +189,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.9. Integrations
1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
-1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ce/user/project/integrations/jira.html)
1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html)
-1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
-1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Bamboo with GitLab](https://docs.gitlab.com/ce/user/project/integrations/bamboo.html)
+1. [How to Integrate Slack with GitLab](https://docs.gitlab.com/ce/user/project/integrations/slack.html)
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 20e7ea1987f..ec565c3e7bf 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -30,15 +30,15 @@ A version control [system](https://www.jfrog.com/open-source/#os-arti) for non-t
### Artifacts
-Objects (usually binary and large) created by a build process. These can include use cases, class diagrams, requirements and design documents.
+Objects (usually binary and large) created by a build process. These can include use cases, class diagrams, requirements and design documents.
### Atlassian
-A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo.
+A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo.
### Audit Log
-Also called an [audit trail](https://en.wikipedia.org/wiki/Audit_trail), an audit log is a document that records an event in an IT system.
+Also called an [audit trail](https://en.wikipedia.org/wiki/Audit_trail), an audit log is a document that records an event in an IT system.
### Auto Defined User Group
@@ -55,7 +55,7 @@ Entry level [subscription](https://about.gitlab.com/pricing/) for GitLab EE curr
### Bitbucket
-Atlassian's web hosting service for Git and Mercurial Projects. Read about [migrating](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_bitbucket.html) from BitBucket to a GitLab instance.
+Atlassian's web hosting service for Git and Mercurial Projects. Read about [migrating](https://docs.gitlab.com/ce/workflow/importing/import_projects_from_bitbucket.html) from BitBucket to a GitLab instance.
### Branch
@@ -65,8 +65,8 @@ A branch is a parallel version of a repository. This allows you to work on the r
Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
-### Build triggers
-These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) build triggers.
+### Job triggers
+These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) job triggers.
### CEPH
@@ -74,7 +74,7 @@ These protect your code base against breaks, for instance when a team is working
### ChatOps
-The ability to [initiate an action](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/1412) from chat. ChatBots run in your chat application and give you the ability to do "anything" from chat.
+The ability to [initiate an action](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/1412) from chat. ChatBots run in your chat application and give you the ability to do "anything" from chat.
### Clone
@@ -82,7 +82,7 @@ A [copy](https://git-scm.com/docs/git-clone) of a repository stored on your mach
### Code Review
-Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
+Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
### Code Snippet
@@ -140,7 +140,7 @@ A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)s
For us at GitLab, this means a software developer, or someone who makes software. It is also one of the levels of access in our multi-level approval system.
-### DevOps
+### DevOps
The intersection of software engineering, quality assurance, and technology operations. Explore more DevOps topics in the [glossary by XebiaLabs](https://xebialabs.com/glossary/)
@@ -160,7 +160,7 @@ A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of
### ElasticSearch
-Elasticsearch is a flexible, scalable and powerful search service. When [enabled](https://gitlab.com/help/integration/elasticsearch.md), it helps keep GitLab's search fast when dealing with a huge amount of data.
+Elasticsearch is a flexible, scalable and powerful search service. When [enabled](https://gitlab.com/help/integration/elasticsearch.md), it helps keep GitLab's search fast when dealing with a huge amount of data.
### Emacs
@@ -174,7 +174,7 @@ A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
### Git Attributes
-A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text file that gives attributes to pathnames.
+A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text file that gives attributes to pathnames.
### Git Hooks
@@ -209,7 +209,7 @@ Our free SaaS for public and private repositories.
Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
### GitLab Pages
-These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
+These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
### Gitolite
@@ -253,7 +253,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
### Jira
@@ -289,7 +289,7 @@ Allows you to synchronize the members of a GitLab group with one or more LDAP gr
### Load Balancer
-A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
+A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
### Git Large File Storage (LFS)
@@ -301,7 +301,7 @@ An operating system like Windows or OS X. It is mostly used by software develope
### Markdown
-A lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name. Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. Checkout GitLab's [Markdown guide](https://gitlab.com/help/user/markdown.md).
+A lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name. Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. Checkout GitLab's [Markdown guide](https://gitlab.com/help/user/markdown.md).
### Maria DB
@@ -313,11 +313,11 @@ Name of the [default branch](https://git-scm.com/book/en/v1/Git-Branching-What-a
### Mattermost
-An open source, self-hosted messaging alternative to Slack. View GitLab's Mattermost [feature](https://gitlab.com/gitlab-org/gitlab-mattermost).
+An open source, self-hosted messaging alternative to Slack. View GitLab's Mattermost [feature](https://gitlab.com/gitlab-org/gitlab-mattermost).
### Mercurial
-A free distributed version control system similar to and a competitor with Git.
+A free distributed version control system similar to and a competitor with Git.
### Merge
@@ -325,7 +325,7 @@ Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-m
### Merge Conflict
-[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
+[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
### Meteor
@@ -345,7 +345,7 @@ A type of software license. It lets people do anything with your code with prope
### Mondo Rescue
-A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
+A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
### MySQL
@@ -361,7 +361,7 @@ A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). I
### OAuth
-An open standard for authorization, commonly used as a way for internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password. GitLab [is](https://docs.gitlab.com/ce/integration/oauth_provider.html) an OAuth2 authentication service provider.
+An open standard for authorization, commonly used as a way for internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password. GitLab [is](https://docs.gitlab.com/ce/integration/oauth_provider.html) an OAuth2 authentication service provider.
### Omnibus Packages
@@ -371,13 +371,13 @@ A way to [package different services and tools](https://docs.gitlab.com/omnibus/
On your own server. In GitLab, this [refers](https://about.gitlab.com/2015/02/12/why-ship-on-premises-in-the-saas-era/) to the ability to download GitLab EE/GitLab CE and host it on your own server rather than using GitLab.com, which is hosted by GitLab Inc's servers.
-### Open Core
+### Open Core
GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-core-github-is-closed-source/). Coined by Andrew Lampitt in 2008, the [open core model](https://en.wikipedia.org/wiki/Open_core) primarily involves offering a "core" or feature-limited version of a software product as free and open-source software, while offering "commercial" versions or add-ons as proprietary software.
### Open Source Software
-Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### Owner
@@ -405,7 +405,7 @@ GitLab Premium EE [subscription](https://about.gitlab.com/pricing/) that include
### PostgreSQL
-An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Touted as the most advanced open source database, it is one of two database management systems [supported by](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/database.md) GitLab, the other being MySQL.
+An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Touted as the most advanced open source database, it is one of two database management systems [supported by](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/database.md) GitLab, the other being MySQL.
### Protected Branches
@@ -421,7 +421,7 @@ A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
### Push
-Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
+Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
### RE Read Only
@@ -429,7 +429,7 @@ Permissions to see a file and its contents, but not change it.
### Rebase
-In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
+In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
### (Git) Repository
@@ -449,7 +449,7 @@ An open source chat application for teams, RocketChat is very similar to Slack b
### Route Table
-A route table contains rules (called routes) that determine where network traffic is directed. Each [subnet in a VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html) must be associated with a route table.
+A route table contains rules (called routes) that determine where network traffic is directed. Each [subnet in a VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html) must be associated with a route table.
### Runners
@@ -477,15 +477,15 @@ The board used to track the status and progress of each of the sprint backlog it
### Shell
-Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
### Single-tenant
-The tenant purchases their own copy of the software and the software can be customized to meet the specific and needs of that customer. [GitHost.io](https://about.gitlab.com/handbook/positioning-faq/) is our provider of single-tenant 'managed cloud' GitLab instances.
+The tenant purchases their own copy of the software and the software can be customized to meet the specific and needs of that customer. [GitHost.io](https://about.gitlab.com/handbook/positioning-faq/) is our provider of single-tenant 'managed cloud' GitLab instances.
### Slack
-Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
+Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
### Slave Servers
@@ -529,7 +529,7 @@ A program that allows you to perform superuser/administrator actions on Unix Ope
### Subversion (SVN)
-An open source version control system. Read about [migrating from SVN](https://docs.gitlab.com/ce/workflow/importing/migrating_from_svn.html) to GitLab using SubGit.
+An open source version control system. Read about [migrating from SVN](https://docs.gitlab.com/ce/workflow/importing/migrating_from_svn.html) to GitLab using SubGit.
### Tag
@@ -545,7 +545,7 @@ An open source project management and bug tracking web [application](https://tra
### Untracked files
-New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
### User
@@ -553,11 +553,11 @@ Anyone interacting with the software.
### Version Control Software (VCS)
-Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
### Virtual Private Cloud (VPC)
-An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
+An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
### Virtual private server (VPS)
@@ -565,15 +565,15 @@ A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold a
### VM Instance
-In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
### Waterfall
-A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the complete software to the customer that meets all the requirements they specified.
+A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the complete software to the customer that meets all the requirements they specified.
### Webhooks
-A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
+A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integrations/webhooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
### Wiki
@@ -585,5 +585,5 @@ A [website/system](http://www.wiki.com/) that allows for collaborative editing o
### YAML
-A human-readable data serialization [language](http://www.yaml.org/about.html) that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail.
+A human-readable data serialization [language](http://www.yaml.org/about.html) that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail.
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 6e415e4d219..567dadb3b47 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -167,12 +167,11 @@ 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)
- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
-- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
+- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/)
- Get to know the GitLab Codebase by reading through the source code:
- Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 1ee615432aa..3ed601625cf 100755
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -5,4 +5,4 @@
3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
5. Code School tutorial [http://try.github.io/](http://try.github.io/)
-6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
+6. Contact Us at `subscribers@gitlab.com`
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 35afe73708f..9e38df26b6a 100755
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -389,4 +389,4 @@ GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/gui
Pro git book [http://git-scm.com/book](http://git-scm.com/book)
Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
Code School tutorial [http://try.github.io/](http://try.github.io/)
-Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
+Contact Us at `subscribers@gitlab.com`
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index fb70eaacbc9..97cd277b424 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.6 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index ce46b57c09a..a890aa885d5 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.9 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index 6ac83f3b60d..e32508745a2 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,5 +1,5 @@
# From 3.0 to 3.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
**IMPORTANT!**
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index df53ed6de83..b370464390e 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,5 +1,5 @@
# From 3.1 to 4.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index c66c6dd0fd8..7124424bb60 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,5 +1,5 @@
# From 4.0 to 4.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 97367c5f347..8ed5b333a2e 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,5 +1,5 @@
# From 4.1 to 4.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
## 1. Stop server & Resque
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 7654f4a0131..1ec39218ba8 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,5 +1,5 @@
# From 4.2 to 5.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index c19a819ab5a..9c9950fb2c6 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,5 +1,5 @@
# From 5.0 to 5.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 625fcc33852..2aab47d2d7c 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index 547d453914c..e80f1b89c63 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
Also works starting from 5.2.
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index c992c69678e..1ee175383da 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.1 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index fe8990b6843..2ae50510f63 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,5 +1,5 @@
# From 5.2 to 5.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 5f82ad7d444..842e3bb6791 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.3 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index f0fee634322..44715984f0c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.4 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 409faf30902..0c672abeb05 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,5 +1,5 @@
# From 6.0 to 6.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index 150c7ae1c83..d3760cf0619 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,5 +1,5 @@
# From 6.1 to 6.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
**You should update to 6.1 before installing 6.2 so all the necessary conversions are run.**
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index b96dfb8add7..91105de2e29 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,5 +1,5 @@
# From 6.2 to 6.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
**Requires version: 6.1 or 6.2.**
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 37028be055f..20b58ed8b25 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,5 +1,5 @@
# From 6.3 to 6.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 982381a4db0..5ee0f040b5d 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,5 +1,5 @@
# From 6.4 to 6.5
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index bbed2b30215..fa3712f83ad 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,5 +1,5 @@
# From 6.5 to 6.6
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 8e82942a1a0..9c85ed091c5 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,5 +1,5 @@
# From 6.6 to 6.7
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 4fb90639f16..687c1265d9b 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,5 +1,5 @@
# From 6.7 to 6.8
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index b9b8b63f652..0205b0c896a 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,5 +1,5 @@
# From 6.8 to 6.9
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 5352fd52f93..4b6e3989893 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,5 +1,5 @@
# From 6.9 to 7.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index f170a0021b7..1e39fe47ef9 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,5 +1,5 @@
# From 6.x or 7.x to 7.14
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14.
@@ -222,7 +222,7 @@ If all items are green, then congratulations upgrade complete!
When using Google omniauth login, changes of the Google account required.
Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/).
-More details can be found at the [integration documentation](../../../master/doc/integration/google.md).
+More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/integration/google.md).
## 12. Optional optimizations for GitLab setups with MySQL databases
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 71f39c44077..2e9457aa142 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,5 +1,5 @@
# From 7.0 to 7.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
### 0. Backup
@@ -136,3 +136,5 @@ 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/7-1-stable/config/gitlab.yml.example
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index 88cb63d7d41..e5045b5570f 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,5 +1,5 @@
# From 7.1 to 7.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
## Editable labels
@@ -135,3 +135,5 @@ 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/7-2-stable/config/gitlab.yml.example
diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md
index 79bc6de1e46..89213ba7178 100644
--- a/doc/update/7.10-to-7.11.md
+++ b/doc/update/7.10-to-7.11.md
@@ -65,7 +65,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-10-stable:config/gitlab.yml.example origin/7-11-stable:config/gitlab.yml.example
@@ -101,3 +101,5 @@ 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/7-11-stable/config/gitlab.yml.example
diff --git a/doc/update/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md
index cc14a135926..3865186918c 100644
--- a/doc/update/7.11-to-7.12.md
+++ b/doc/update/7.11-to-7.12.md
@@ -91,7 +91,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-11-stable:config/gitlab.yml.example origin/7-12-stable:config/gitlab.yml.example
@@ -127,3 +127,5 @@ 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/7-12-stable/config/gitlab.yml.example
diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md
index 57ebe3261b6..4c8d8f1f741 100644
--- a/doc/update/7.12-to-7.13.md
+++ b/doc/update/7.12-to-7.13.md
@@ -91,7 +91,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-12-stable:config/gitlab.yml.example origin/7-13-stable:config/gitlab.yml.example
@@ -127,3 +127,5 @@ 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/7-13-stable/config/gitlab.yml.example
diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md
index 6dd9727fb49..934898da5a1 100644
--- a/doc/update/7.13-to-7.14.md
+++ b/doc/update/7.13-to-7.14.md
@@ -91,7 +91,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-13-stable:config/gitlab.yml.example origin/7-14-stable:config/gitlab.yml.example
@@ -127,3 +127,5 @@ 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/7-14-stable/config/gitlab.yml.example
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 117e2afaaa0..25fa6d93f06 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -143,7 +143,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/7-14-stable:config/gitlab.yml.example origin/8-0-stable:config/gitlab.yml.example
@@ -227,3 +227,5 @@ this is likely due to an outdated Nginx or Apache configuration, or a missing or
misconfigured `gitlab-git-http-server` instance. Double-check that you correctly
completed [Step 5](#5-install-gitlab-git-http-server) to install the daemon and
[Step 8](#new-nginx-configuration) to reconfigure Nginx.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/config/gitlab.yml.example
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index 18f77d6396e..d3391ddd225 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,5 +1,5 @@
# From 7.2 to 7.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
### 0. Backup
@@ -143,3 +143,5 @@ 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/7-3-stable/config/gitlab.yml.example
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 53e739c06fb..6d632dc3c8e 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,5 +1,5 @@
# From 7.3 to 7.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
### 0. Stop server
@@ -75,7 +75,7 @@ sudo -u git -H editor config/unicorn.rb
#### MySQL Databases: Update database.yml config file
-* Add `collation: utf8_general_ci` to `config/database.yml` as seen in [config/database.yml.mysql](/config/database.yml.mysql)
+* Add `collation: utf8_general_ci` to `config/database.yml` as seen in [config/database.yml.mysql][mysql]:
```
sudo -u git -H editor config/database.yml
@@ -192,6 +192,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/7-4-stable/config/gitlab.yml.example
+[mysql]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/config/database.yml.mysql
diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md
index 673eab3c56e..ec50706d421 100644
--- a/doc/update/7.4-to-7.5.md
+++ b/doc/update/7.4-to-7.5.md
@@ -73,8 +73,8 @@ git diff origin/7-4-stable:config/gitlab.yml.example origin/7-5-stable:config/gi
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your setting
### 6. Start application
@@ -106,3 +106,7 @@ 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/7-5-stable/config/gitlab.yml.example
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-5-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md
index 35cd437fdc4..331f5de080e 100644
--- a/doc/update/7.5-to-7.6.md
+++ b/doc/update/7.5-to-7.6.md
@@ -67,7 +67,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-5-stable:config/gitlab.yml.example origin/7-6-stable:config/gitlab.yml.example
@@ -75,12 +75,12 @@ git diff origin/7-5-stable:config/gitlab.yml.example origin/7-6-stable:config/gi
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your setting
#### Setup time zone (optional)
-Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`][app] (unlikely), unset it.
### 6. Start application
@@ -112,3 +112,8 @@ 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/7-6-stable/config/gitlab.yml.example
+[app]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/config/application.rb
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-6-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md
index 910c7dcdd3c..918b10fbd95 100644
--- a/doc/update/7.6-to-7.7.md
+++ b/doc/update/7.6-to-7.7.md
@@ -67,7 +67,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](/config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-6-stable:config/gitlab.yml.example origin/7-7-stable:config/gitlab.yml.example
@@ -75,12 +75,12 @@ git diff origin/7-6-stable:config/gitlab.yml.example origin/7-7-stable:config/gi
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your setting
#### Setup time zone (optional)
-Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`][app] (unlikely), unset it.
### 6. Start application
@@ -101,7 +101,7 @@ If all items are green, then congratulations upgrade is complete!
### 8. GitHub settings (if applicable)
-If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it
+If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it
only contains a root URL (ex. `https://gitlab.example.com/`)
## Things went south? Revert to previous version (7.6)
@@ -117,3 +117,8 @@ 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/7-7-stable/config/gitlab.yml.example
+[app]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/config/application.rb
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md
index 46ca163c1bb..84e0464a824 100644
--- a/doc/update/7.7-to-7.8.md
+++ b/doc/update/7.7-to-7.8.md
@@ -67,7 +67,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-7-stable:config/gitlab.yml.example origin/7-8-stable:config/gitlab.yml.example
@@ -75,13 +75,13 @@ git diff origin/7-7-stable:config/gitlab.yml.example origin/7-8-stable:config/gi
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings.
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings.
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your settings.
* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section.
#### Setup time zone (optional)
-Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`][app] (unlikely), unset it.
### 6. Start application
@@ -118,3 +118,8 @@ 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/7-8-stable/config/gitlab.yml.example
+[app]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/application.rb
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md
index 6ffa21c6141..b0dc2ba1dbb 100644
--- a/doc/update/7.8-to-7.9.md
+++ b/doc/update/7.8-to-7.9.md
@@ -69,7 +69,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-8-stable:config/gitlab.yml.example origin/7-9-stable:config/gitlab.yml.example
@@ -77,13 +77,13 @@ git diff origin/7-8-stable:config/gitlab.yml.example origin/7-9-stable:config/gi
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings.
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings.
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your settings.
* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section.
#### Setup time zone (optional)
-Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`][app] (unlikely), unset it.
### 6. Start application
@@ -120,3 +120,8 @@ 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/7-9-stable/config/gitlab.yml.example
+[app]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/application.rb
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md
index d1179dc2ec7..8f7f84b41ba 100644
--- a/doc/update/7.9-to-7.10.md
+++ b/doc/update/7.9-to-7.10.md
@@ -65,7 +65,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-9-stable:config/gitlab.yml.example origin/7-10-stable:config/gitlab.yml.example
@@ -73,13 +73,13 @@ git diff origin/7-9-stable:config/gitlab.yml.example origin/7-10-stable:config/g
#### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings.
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings.
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`][nginx] but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`][nginx-ssl] but with your settings.
* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section.
#### Setup time zone (optional)
-Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`][app] (unlikely), unset it.
### 6. Start application
@@ -116,3 +116,8 @@ 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/7-10-stable/config/gitlab.yml.example
+[app]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/application.rb
+[nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab
+[nginx-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab-ssl
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index bfb83cf79b1..6ee0c0656ee 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -108,7 +108,7 @@ For Ubuntu 16.04.1 LTS:
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-0-stable:config/gitlab.yml.example origin/8-1-stable:config/gitlab.yml.example
@@ -173,3 +173,5 @@ If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of
### "You appear to have cloned an empty repository."
See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-1-stable/config/gitlab.yml.example
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 7f36ce00e96..4c9ff5c5c0a 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -125,7 +125,7 @@ For Ubuntu 16.04.1 LTS:
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-1-stable:config/gitlab.yml.example origin/8-2-stable:config/gitlab.yml.example
@@ -190,3 +190,5 @@ If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of
### "You appear to have cloned an empty repository."
See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-2-stable/config/gitlab.yml.example
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 119c5f475e4..e5e3cd395df 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -114,7 +114,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example
@@ -195,3 +195,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-11-stable/config/gitlab.yml.example
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index cddfa7e3e01..d6b3b0ffa5a 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-11-stable:config/gitlab.yml.example origin/8-12-stable:config/gitlab.yml.example
@@ -203,3 +203,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-12-stable/config/gitlab.yml.example
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 8c0d3f78b55..75956aeb360 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-12-stable:config/gitlab.yml.example origin/8-13-stable:config/gitlab.yml.example
@@ -203,3 +203,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-13-stable/config/gitlab.yml.example
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index c64d3407461..327ecb7cdc2 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-13-stable:config/gitlab.yml.example origin/8-14-stable:config/gitlab.yml.example
@@ -203,3 +203,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-14-stable/config/gitlab.yml.example
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index b1e3b116548..a68fe3bb605 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -122,7 +122,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`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
cd /home/git/gitlab
@@ -235,3 +235,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-15-stable/config/gitlab.yml.example
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
index 2695a16ac0b..9f8f0f714d4 100644
--- a/doc/update/8.15-to-8.16.md
+++ b/doc/update/8.15-to-8.16.md
@@ -124,7 +124,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`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
cd /home/git/gitlab
@@ -237,3 +237,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-16-stable/config/gitlab.yml.example
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
new file mode 100644
index 00000000000..954109ba18f
--- /dev/null
+++ b/doc/update/8.16-to-8.17.md
@@ -0,0 +1,256 @@
+# From 8.16 to 8.17
+
+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/>
+
+### 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 8-17-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-17-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
+
+# Install/update frontend asset dependencies
+sudo -u git -H npm install --production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile 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 v4.1.1
+```
+
+### 9. Update configuration files
+
+#### 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`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:config/gitlab.yml.example origin/8-17-stable:config/gitlab.yml.example
+```
+
+#### 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-16-stable:lib/support/nginx/gitlab-ssl origin/8-17-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-16-stable:lib/support/nginx/gitlab origin/8-17-stable:lib/support/nginx/gitlab
+```
+
+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/8-17-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
+
+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.16)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.15 to 8.16](8.15-to-8.16.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/8-17-stable/config/gitlab.yml.example
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..1fe38cf8d2a
--- /dev/null
+++ b/doc/update/8.17-to-9.0.md
@@ -0,0 +1,321 @@
+# 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
+
+# Install/update frontend asset dependencies
+sudo -u git -H npm install --production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile 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 are 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
+
+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. Upate 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
+
+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
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index dd3fdafd8d1..f28896c2227 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -114,7 +114,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-2-stable:config/gitlab.yml.example origin/8-3-stable:config/gitlab.yml.example
@@ -211,3 +211,5 @@ If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of
### "You appear to have cloned an empty repository."
See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/config/gitlab.yml.example
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index e62d894609a..8b89455ca87 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -84,7 +84,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-3-stable:config/gitlab.yml.example origin/8-4-stable:config/gitlab.yml.example
@@ -98,7 +98,7 @@ We updated the init script for GitLab in order to set a specific PATH for gitlab
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
-
+
For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
@@ -135,3 +135,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-4-stable/config/gitlab.yml.example
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 678cc69d773..0eedfaee2db 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -88,7 +88,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-4-stable:config/gitlab.yml.example origin/8-5-stable:config/gitlab.yml.example
@@ -119,7 +119,7 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
-
+
For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
@@ -156,3 +156,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-5-stable/config/gitlab.yml.example
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index a76346516b9..851056161bb 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -107,7 +107,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
@@ -175,3 +175,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-6-stable/config/gitlab.yml.example
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index 05ef4e61759..34c727260aa 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -88,7 +88,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-6-stable:config/gitlab.yml.example origin/8-7-stable:config/gitlab.yml.example
@@ -164,3 +164,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-7-stable/config/gitlab.yml.example
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 8ce434e5f78..6feeb1919de 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -88,7 +88,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-7-stable:config/gitlab.yml.example origin/8-8-stable:config/gitlab.yml.example
@@ -164,3 +164,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-8-stable/config/gitlab.yml.example
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index aa077316bbe..61cdf8854d4 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -104,7 +104,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-8-stable:config/gitlab.yml.example origin/8-9-stable:config/gitlab.yml.example
@@ -193,3 +193,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-9-stable/config/gitlab.yml.example
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index bb2c79fbb84..d6b2f11d49a 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -104,7 +104,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `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`:
```sh
git diff origin/8-9-stable:config/gitlab.yml.example origin/8-10-stable:config/gitlab.yml.example
@@ -193,3 +193,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-10-stable/config/gitlab.yml.example
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
index 9336dee7451..f4078876fab 100644
--- a/doc/user/account/security.md
+++ b/doc/user/account/security.md
@@ -1 +1 @@
-This document was moved to [profile](../profile/index.md#security).
+This document was moved to [profile](../profile/account/index.md).
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 34e2e557f89..eb6f915f3f4 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -2,19 +2,37 @@
## Maximum artifacts size
-The maximum size of the [build 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 build.
+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.
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
-[art-yml]: ../../../administration/build_artifacts.md
+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/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 4b540473a6e..603b826e7f2 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,5 +1,20 @@
# Sign-up restrictions
+You can block email addresses of specific domains, or whitelist only some
+specifc domains via the **Application Settings** in the Admin area.
+
+>**Note**: These restrictions are only applied during sign-up. An admin is
+able to add add a user through the admin panel with a disallowed domain. Also
+note that the users can change their email addresses after signup to
+disallowed domains.
+
+## Whitelist email domains
+
+> [Introduced][ce-598] in GitLab 7.11.0
+
+You can restrict users to only signup using email addresses matching the given
+domains list.
+
## Blacklist email domains
> [Introduced][ce-5259] in GitLab 8.10.
@@ -9,13 +24,16 @@ from creating an account on your GitLab server. This is particularly useful to
prevent spam. Disposable email addresses are usually used by malicious users to
create dummy accounts and spam issues.
+## Settings
+
This feature can be activated via the **Application Settings** in the Admin area,
and you have the option of entering the list manually, or uploading a file with
the list.
-The blacklist accepts wildcards, so you can use `*.test.com` to block every
-`test.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains
-should be separated by a whitespace, semicolon, comma, or a new line.
+Both whitelist and blacklist accept wildcards, so for example, you can use
+`*.company.com` to accept every `company.com` subdomain, or `*.io` to block all
+domains ending in `.io`. Domains should be separated by a whitespace,
+semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 008872b59a7..97de428d11d 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -237,23 +237,24 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
-| input | references |
-|:-----------------------|:--------------------------- |
-| `@user_name` | specific user |
-| `@group_name` | specific group |
-| `@all` | entire team |
-| `#123` | issue |
-| `!123` | merge request |
-| `$123` | snippet |
-| `~123` | label by ID |
-| `~bug` | one-word label by name |
-| `~"feature request"` | multi-word label by name |
-| `%123` | milestone by ID |
-| `%v1.23` | one-word milestone by name |
-| `%"release candidate"` | multi-word milestone by name |
-| `9ba12248` | specific commit |
-| `9ba12248...b19a04f5` | commit range comparison |
-| `[README](doc/README)` | repository file references |
+| input | references |
+|:---------------------------|:--------------------------------|
+| `@user_name` | specific user |
+| `@group_name` | specific group |
+| `@all` | entire team |
+| `#123` | issue |
+| `!123` | merge request |
+| `$123` | snippet |
+| `~123` | label by ID |
+| `~bug` | one-word label by name |
+| `~"feature request"` | multi-word label by name |
+| `%123` | milestone by ID |
+| `%v1.23` | one-word milestone by name |
+| `%"release candidate"` | multi-word milestone by name |
+| `9ba12248` | specific commit |
+| `9ba12248...b19a04f5` | commit range comparison |
+| `[README](doc/README)` | repository file references |
+| `[README](doc/README#L13)` | repository file line references |
GFM also recognizes certain cross-project references:
@@ -430,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.~~
```
@@ -523,35 +524,12 @@ There are two ways to create links, inline-style and reference-style.
[1]: http://slashdot.org
[link text itself]: https://www.reddit.com
-[I'm an inline-style link](https://www.google.com)
-
-[I'm a reference-style link][Arbitrary case-insensitive reference text]
-
-[I'm a relative reference to a repository file](LICENSE)[^1]
-
-[I am an absolute reference within the repository](/doc/user/markdown.md)
-
-[I link to the Milestones page](/../milestones)
-
-[You can use numbers for reference-style link definitions][1]
-
-Or leave it empty and use the [link text itself][]
-
-Some text to show that the reference links can follow later.
-
-[arbitrary case-insensitive reference text]: https://www.mozilla.org
-[1]: http://slashdot.org
-[link text itself]: https://www.reddit.com
-
-**Note**
-
-Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
-
-`[I'm a reference-style link](style)`
-
+>**Note:**
+Relative links do not allow referencing project files in a wiki page or wiki
+page in a project file. The reason for this is that, in GitLab, wiki is always
+a separate Git repository. For example, `[I'm a reference-style link](style)`
will point the link to `wikis/style` when the link is inside of a wiki markdown file.
-
### Images
Here's our logo (hover to see the title text):
@@ -598,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 678fc3ffd1f..b49a244160a 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -22,9 +22,9 @@ The following table depicts the various user permission levels in a project.
| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of builds | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
-| See a build log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse build artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| See a list of jobs | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| See a job log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse job artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
@@ -46,7 +46,7 @@ The following table depicts the various user permission levels in a project.
| Remove non-protected branches | | | ✓ | ✓ | ✓ |
| Add tags | | | ✓ | ✓ | ✓ |
| Write a wiki | | | ✓ | ✓ | ✓ |
-| Cancel and retry builds | | | ✓ | ✓ | ✓ |
+| Cancel and retry jobs | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
@@ -60,13 +60,16 @@ The following table depicts the various user permission levels in a project.
| Add deploy keys to project | | | | ✓ | ✓ |
| Configure project hooks | | | | ✓ | ✓ |
| Manage runners | | | | ✓ | ✓ |
-| Manage build triggers | | | | ✓ | ✓ |
+| Manage job triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Manage pages | | | | ✓ | ✓ |
+| Manage pages domains and certificates | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Force push to protected branches [^3] | | | | | |
| Remove protected branches [^3] | | | | | |
+| Remove pages | | | | | ✓ |
## Group
@@ -131,8 +134,8 @@ instance and project. In addition, all admins can use the admin interface under
| Action | Guest, Reporter | Developer | Master | Admin |
|---------------------------------------|-----------------|-------------|----------|--------|
-| See commits and builds | ✓ | ✓ | ✓ | ✓ |
-| Retry or cancel build | | ✓ | ✓ | ✓ |
+| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
+| Retry or cancel job | | ✓ | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
@@ -141,18 +144,18 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
-### Builds permissions
+### Jobs permissions
>**Note:**
-GitLab 8.12 has a completely redesigned build permissions system.
+GitLab 8.12 has a completely redesigned job permissions system.
Read all about the [new model and its implications][new-mod].
-This table shows granted privileges for builds triggered by specific types of
+This table shows granted privileges for jobs triggered by specific types of
users:
| Action | Guest, Reporter | Developer | Master | Admin |
|---------------------------------------------|-----------------|-------------|----------|--------|
-| Run CI build | | ✓ | ✓ | ✓ |
+| Run CI job | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ |
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index cc688a7f99c..eaa39a0c4ea 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -206,12 +206,12 @@ Sign in and re-enable two-factor authentication as soon as possible.
## Note to GitLab administrators
- You need to take special care to that 2FA keeps working after
-[restoring a GitLab backup](../raketasks/backup_restore.md).
+[restoring a GitLab backup](../../../raketasks/backup_restore.md).
- To ensure 2FA authorizes correctly with TOTP server, you may want to ensure
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/
diff --git a/doc/user/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
index 88f1863dddb..514c729b37d 100644
--- a/doc/user/project/builds/artifacts.md
+++ b/doc/user/project/builds/artifacts.md
@@ -1,136 +1 @@
-# Introduction to build artifacts
-
->**Notes:**
->- Since GitLab 8.2 and GitLab Runner 0.7.0, build artifacts that are created by
- GitLab Runner are uploaded to GitLab and are downloadable as a single archive
- (`tar.gz`) using the GitLab UI.
->- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
- changed to `ZIP`, and it is now possible to browse its contents, with the added
- ability of downloading the files separately.
->- The artifacts browser will be available only for new artifacts that are sent
- to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
- browse old artifacts already uploaded to GitLab.
->- This is the user documentation. For the administration guide see
- [administration/build_artifacts.md](../../../administration/build_artifacts.md).
-
-Artifacts is a list of files and directories which are attached to a build
-after it completes successfully. This feature is enabled by default in all GitLab installations.
-
-## Defining artifacts in `.gitlab-ci.yml`
-
-A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
-the following:
-
-```yaml
-pdf:
- script: xelatex mycv.tex
- artifacts:
- paths:
- - mycv.pdf
-```
-
-A job named `pdf` calls the `xelatex` command in order to build a pdf file from
-the latex source file `mycv.tex`. We then define the `artifacts` paths which in
-turn are defined with the `paths` keyword. All paths to files and directories
-are relative to the repository that was cloned during the build.
-
-For more examples on artifacts, follow the artifacts reference in
-[`.gitlab-ci.yml` documentation](../../../ci/yaml/README.md#artifacts).
-
-## Browsing build artifacts
-
-When GitLab receives an artifacts archive, an archive metadata file is also
-generated. This metadata file describes all the entries that are located in the
-artifacts archive itself. The metadata file is in a binary format, with
-additional GZIP compression.
-
-GitLab does not extract the artifacts archive in order to save space, memory
-and disk I/O. It instead inspects the metadata file which contains all the
-relevant information. This is especially important when there is a lot of
-artifacts, or an archive is a very large file.
-
----
-
-After a build finishes, if you visit the build's specific page, you can see
-that there are two buttons. One is for downloading the artifacts archive and
-the other for browsing its contents.
-
-![Build artifacts browser button](img/build_artifacts_browser_button.png)
-
----
-
-The archive browser shows the name and the actual file size of each file in the
-archive. If your artifacts contained directories, then you are also able to
-browse inside them.
-
-Below you can see how browsing looks like. In this case we have browsed inside
-the archive and at this point there is one directory and one HTML file.
-
-![Build artifacts browser](img/build_artifacts_browser.png)
-
----
-
-## Downloading build artifacts
-
->**Note:**
-GitLab does not extract the entire artifacts archive to send just a single file
-to the user. When clicking on a specific file, [GitLab Workhorse] extracts it
-from the archive and the download begins. This implementation saves space,
-memory and disk I/O.
-
-If you need to download the whole archive, there are buttons in various places
-inside GitLab that make that possible.
-
-1. While on the pipelines page, you can see the download icon for each build's
- artifacts archive in the right corner:
-
- ![Build artifacts in Pipelines page](img/build_artifacts_pipelines_page.png)
-
-1. While on the builds page, you can see the download icon for each build's
- artifacts archive in the right corner:
-
- ![Build artifacts in Builds page](img/build_artifacts_builds_page.png)
-
-1. While inside a specific build, you are presented with a download button
- along with the one that browses the archive:
-
- ![Build artifacts browser button](img/build_artifacts_browser_button.png)
-
-1. And finally, when browsing an archive you can see the download button at
- the top right corner:
-
- ![Build artifacts browser](img/build_artifacts_browser.png)
-
-## Downloading the latest build artifacts
-
-It is possible to download the latest artifacts of a build via a well known URL
-so you can use it for scripting purposes.
-
-The structure of the URL is the following:
-
-```
-https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
-```
-
-For example, to download the latest artifacts of the job named `rspec 6 20` of
-the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
-namespace, the URL would be:
-
-```
-https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
-```
-
-The latest builds are also exposed in the UI in various places. Specifically,
-look for the download button in:
-
-- the main project's page
-- the branches page
-- the tags page
-
-If the latest build has failed to upload the artifacts, you can see that
-information in the UI.
-
-![Latest artifacts button](img/build_latest_artifacts_browser.png)
-
-
-[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
+This document was moved to [pipelines/job_artifacts](../pipelines/job_artifacts.md).
diff --git a/doc/user/project/builds/img/build_artifacts_browser.png b/doc/user/project/builds/img/build_artifacts_browser.png
deleted file mode 100644
index 686273948d6..00000000000
--- a/doc/user/project/builds/img/build_artifacts_browser.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_browser_button.png b/doc/user/project/builds/img/build_artifacts_browser_button.png
deleted file mode 100644
index 33ef7de0415..00000000000
--- a/doc/user/project/builds/img/build_artifacts_browser_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_builds_page.png b/doc/user/project/builds/img/build_artifacts_builds_page.png
deleted file mode 100644
index 8f75602d592..00000000000
--- a/doc/user/project/builds/img/build_artifacts_builds_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/builds/img/build_artifacts_pipelines_page.png b/doc/user/project/builds/img/build_artifacts_pipelines_page.png
deleted file mode 100644
index 4bbd00ddaa0..00000000000
--- a/doc/user/project/builds/img/build_artifacts_pipelines_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 47a4a3f85d0..b6221620e58 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -1,10 +1,7 @@
# GitLab Container Registry
-> [Introduced][ce-4040] in GitLab 8.8.
-
----
-
>**Notes:**
+> [Introduced][ce-4040] in GitLab 8.8.
- Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
versions earlier than 1.10.
- This document is about the user guide. To learn how to enable GitLab Container
@@ -98,8 +95,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building Docker images by
-following the [Using Docker Build](../ci/docker/using_docker_build.md)
-and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+following the [Using Docker Build](../../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
@@ -252,4 +249,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://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#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/img/issue_board.png b/doc/user/project/img/issue_board.png
index 95e8532e908..b636cb294b8 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png
deleted file mode 100644
index fbb67b9c18f..00000000000
--- a/doc/user/project/img/issue_board_search_backlog.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
index 5bfdac88dde..5318e6ea4a9 100644
--- a/doc/user/project/img/issue_board_welcome_message.png
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png
new file mode 100644
index 00000000000..33049dce74f
--- /dev/null
+++ b/doc/user/project/img/issue_boards_add_issues_modal.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_remove_issue.png b/doc/user/project/img/issue_boards_remove_issue.png
new file mode 100644
index 00000000000..8b3beca97cf
--- /dev/null
+++ b/doc/user/project/img/issue_boards_remove_issue.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png
index 1c05cb8fd36..320e6eb7fee 100644
--- a/doc/user/project/img/protected_branches_devs_can_push.png
+++ b/doc/user/project/img/protected_branches_devs_can_push.png
Binary files differ
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
new file mode 100644
index 00000000000..cad4757f287
--- /dev/null
+++ b/doc/user/project/integrations/bamboo.md
@@ -0,0 +1,59 @@
+# Atlassian Bamboo CI Service
+
+GitLab provides integration with Atlassian Bamboo for continuous integration.
+When configured, pushes to a project will trigger a build in Bamboo automatically.
+Merge requests will also display CI status showing whether the build is pending,
+failed, or completed successfully. It also provides a link to the Bamboo build
+page for more information.
+
+Bamboo doesn't quite provide the same features as a traditional build system when
+it comes to accepting webhooks and commit data. There are a few things that
+need to be configured in a Bamboo build plan before GitLab can integrate.
+
+## Setup
+
+### Complete these steps in Bamboo
+
+1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
+ dropdown.
+1. Select the 'Triggers' tab.
+1. Click 'Add trigger'.
+1. Enter a description such as 'GitLab trigger'
+1. Choose 'Repository triggers the build when changes are committed'
+1. Check one or more repositories checkboxes
+1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
+ whitelist of IP addresses that are allowed to trigger Bamboo builds.
+1. Save the trigger.
+1. In the left pane, select a build stage. If you have multiple build stages
+ you want to select the last stage that contains the git checkout task.
+1. Select the 'Miscellaneous' tab.
+1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
+ in the 'Labels' box.
+1. Save
+
+Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure to trigger builds.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click 'Atlassian Bamboo CI'
+1. Select the 'Active' checkbox.
+1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
+1. Enter the build key from your Bamboo build plan. Build keys are a short,
+ all capital letter, identifier that is unique. It will be something like PR-BLD
+1. If necessary, enter username and password for a Bamboo user that has
+ access to trigger the build plan. Leave these fields blank if you do not require
+ authentication.
+1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
+ will actually trigger a build in Bamboo.
+
+## Troubleshooting
+
+If builds are not triggered, these are a couple of things to keep in mind.
+
+1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
+ IP addresses'.
+1. Remember that GitLab only triggers builds on push events. A commit via the
+ web interface will not trigger CI currently.
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
new file mode 100644
index 00000000000..0b219e84478
--- /dev/null
+++ b/doc/user/project/integrations/bugzilla.md
@@ -0,0 +1,18 @@
+# Bugzilla Service
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services),
+select the **Bugzilla** service and fill in the required details as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+
+Once you have configured and enabled Bugzilla:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Bugzilla product page
+- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
new file mode 100644
index 00000000000..f769dece242
--- /dev/null
+++ b/doc/user/project/integrations/builds_emails.md
@@ -0,0 +1,15 @@
+# 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/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md
new file mode 100644
index 00000000000..18109fc049c
--- /dev/null
+++ b/doc/user/project/integrations/emails_on_push.md
@@ -0,0 +1,20 @@
+# Enabling emails on push
+
+By enabling this service, you will be able to receive email notifications for
+every change that is pushed to your project.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Emails on push** service to configure it.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md
new file mode 100644
index 00000000000..eee779c50d4
--- /dev/null
+++ b/doc/user/project/integrations/hipchat.md
@@ -0,0 +1,53 @@
+# Atlassian HipChat
+
+GitLab provides a way to send HipChat notifications upon a number of events,
+such as when a user pushes code, creates a branch or tag, adds a comment, and
+creates a merge request.
+
+## Setup
+
+GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
+not supported at this time. Note the differences between v1 and v2 tokens:
+
+HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
+token is allowed to send messages to *any* room.
+
+HipChat v2 API has tokens that are can be created using the Integrations tab
+in the Group or Room admin page. By design, these are lightweight tokens that
+allow GitLab to send messages only to *one* room.
+
+### Complete these steps in HipChat
+
+1. Go to: https://admin.hipchat.com/admin
+1. Click on "Group Admin" -> "Integrations".
+1. Find "Build Your Own!" and click "Create".
+1. Select the desired room, name the integration "GitLab", and click "Create".
+1. In the "Send messages to this room by posting this URL" column, you should
+see a URL in the format:
+
+```
+https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+```
+
+HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "HipChat".
+1. Select the "Active" checkbox.
+1. Insert the `token` field from the URL into the `Token` field on the Web page.
+1. Insert the `room` field from the URL into the `Room` field on the Web page.
+1. Save or optionally click "Test Settings".
+
+## Troubleshooting
+
+If you do not see notifications, make sure you are using a HipChat v2 API
+token, not a v1 token.
+
+Note that the v2 token is tied to a specific room. If you want to be able to
+specify arbitrary rooms, you can create an API token for a specific user in
+HipChat under "Account settings" and "API access". Use the `XXX` value under
+`auth_token=XXX`.
diff --git a/doc/user/project/integrations/img/accessing_integrations.png b/doc/user/project/integrations/img/accessing_integrations.png
new file mode 100644
index 00000000000..3b941f64998
--- /dev/null
+++ b/doc/user/project/integrations/img/accessing_integrations.png
Binary files differ
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png
index df301aa1eeb..df301aa1eeb 100644
--- a/doc/project_services/img/emails_on_push_service.png
+++ b/doc/user/project/integrations/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png
index 27dac49260c..27dac49260c 100644
--- a/doc/project_services/img/jira_add_user_to_group.png
+++ b/doc/user/project/integrations/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png
index 06c4e84fc61..06c4e84fc61 100644
--- a/doc/project_services/img/jira_create_new_group.png
+++ b/doc/user/project/integrations/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/user/project/integrations/img/jira_create_new_group_name.png
index bfc0dc6b2e9..bfc0dc6b2e9 100644
--- a/doc/project_services/img/jira_create_new_group_name.png
+++ b/doc/user/project/integrations/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png
index e9c03ed770d..e9c03ed770d 100644
--- a/doc/project_services/img/jira_create_new_user.png
+++ b/doc/user/project/integrations/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png
index 9d64cc57269..9d64cc57269 100644
--- a/doc/project_services/img/jira_group_access.png
+++ b/doc/user/project/integrations/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/user/project/integrations/img/jira_issue_reference.png
index 72c81460df7..72c81460df7 100644
--- a/doc/project_services/img/jira_issue_reference.png
+++ b/doc/user/project/integrations/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/user/project/integrations/img/jira_merge_request_close.png
index 0f82ceba557..0f82ceba557 100644
--- a/doc/project_services/img/jira_merge_request_close.png
+++ b/doc/user/project/integrations/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png
index 8540a427461..8540a427461 100644
--- a/doc/project_services/img/jira_project_name.png
+++ b/doc/user/project/integrations/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png
index 8e073b84ff9..8e073b84ff9 100644
--- a/doc/project_services/img/jira_service.png
+++ b/doc/user/project/integrations/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/user/project/integrations/img/jira_service_close_comment.png
index bb9cd7e3d13..bb9cd7e3d13 100644
--- a/doc/project_services/img/jira_service_close_comment.png
+++ b/doc/user/project/integrations/img/jira_service_close_comment.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/user/project/integrations/img/jira_service_close_issue.png
index c85b1d1dd97..c85b1d1dd97 100644
--- a/doc/project_services/img/jira_service_close_issue.png
+++ b/doc/user/project/integrations/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png
index c74351b57b8..c74351b57b8 100644
--- a/doc/project_services/img/jira_service_page.png
+++ b/doc/user/project/integrations/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png
index f81c5b5fc87..f81c5b5fc87 100644
--- a/doc/project_services/img/jira_user_management_link.png
+++ b/doc/user/project/integrations/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png
index e62fb202613..e62fb202613 100644
--- a/doc/project_services/img/jira_workflow_screenshot.png
+++ b/doc/user/project/integrations/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
index 349a2dc8456..349a2dc8456 100644
--- a/doc/project_services/img/kubernetes_configuration.png
+++ b/doc/user/project/integrations/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/user/project/integrations/img/mattermost_add_slash_command.png
index 7759efa183c..7759efa183c 100644
--- a/doc/project_services/img/mattermost_add_slash_command.png
+++ b/doc/user/project/integrations/img/mattermost_add_slash_command.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/user/project/integrations/img/mattermost_bot_auth.png
index 830b7849f3d..830b7849f3d 100644
--- a/doc/project_services/img/mattermost_bot_auth.png
+++ b/doc/user/project/integrations/img/mattermost_bot_auth.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
index b51798cf10d..b51798cf10d 100644
--- a/doc/project_services/img/mattermost_bot_available_commands.png
+++ b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
Binary files differ
diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png
new file mode 100644
index 00000000000..dd3481bc1f6
--- /dev/null
+++ b/doc/user/project/integrations/img/mattermost_config_help.png
Binary files differ
diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
new file mode 100644
index 00000000000..f52acf4ef3b
--- /dev/null
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/user/project/integrations/img/mattermost_console_integrations.png
index 92a30da5be0..92a30da5be0 100644
--- a/doc/project_services/img/mattermost_console_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_console_integrations.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/user/project/integrations/img/mattermost_gitlab_token.png
index 257018914d2..257018914d2 100644
--- a/doc/project_services/img/mattermost_gitlab_token.png
+++ b/doc/user/project/integrations/img/mattermost_gitlab_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/user/project/integrations/img/mattermost_goto_console.png
index 3354c2a24b4..3354c2a24b4 100644
--- a/doc/project_services/img/mattermost_goto_console.png
+++ b/doc/user/project/integrations/img/mattermost_goto_console.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
index 12766ab2b34..12766ab2b34 100644
--- a/doc/project_services/img/mattermost_slash_command_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/user/project/integrations/img/mattermost_slash_command_token.png
index c38f37c203c..c38f37c203c 100644
--- a/doc/project_services/img/mattermost_slash_command_token.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/user/project/integrations/img/mattermost_team_integrations.png
index 69d4a231e5a..69d4a231e5a 100644
--- a/doc/project_services/img/mattermost_team_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_team_integrations.png
Binary files differ
diff --git a/doc/user/project/integrations/img/project_services.png b/doc/user/project/integrations/img/project_services.png
new file mode 100644
index 00000000000..25b6cd5690b
--- /dev/null
+++ b/doc/user/project/integrations/img/project_services.png
Binary files differ
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/user/project/integrations/img/redmine_configuration.png
index 7b6dd271401..7b6dd271401 100644
--- a/doc/project_services/img/redmine_configuration.png
+++ b/doc/user/project/integrations/img/redmine_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png
new file mode 100644
index 00000000000..379cef9888d
--- /dev/null
+++ b/doc/user/project/integrations/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
new file mode 100644
index 00000000000..527824fc3eb
--- /dev/null
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png
new file mode 100644
index 00000000000..7928fb7d495
--- /dev/null
+++ b/doc/user/project/integrations/img/slack_setup.png
Binary files differ
diff --git a/doc/web_hooks/ssl.png b/doc/user/project/integrations/img/webhooks_ssl.png
index 21ddec4ebdf..21ddec4ebdf 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/user/project/integrations/img/webhooks_ssl.png
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
new file mode 100644
index 00000000000..99093ebaed5
--- /dev/null
+++ b/doc/user/project/integrations/index.md
@@ -0,0 +1,26 @@
+# Project integrations
+
+You can find the available integrations under the **Integrations** page by
+navigating to the cog icon in the upper right corner of your project. You need
+to have at least [master permission][permissions] on the project.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Project services
+
+Project services allow you to integrate GitLab with other applications.
+They are a bit like plugins in that they allow a lot of freedom in
+adding functionality to GitLab.
+
+[Learn more about project services.](project_services.md)
+
+## Project webhooks
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+[Learn more about webhooks.](webhooks.md)
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md
new file mode 100644
index 00000000000..c63ea1316fe
--- /dev/null
+++ b/doc/user/project/integrations/irker.md
@@ -0,0 +1,50 @@
+# Irker IRC Gateway
+
+GitLab provides a way to push update messages to an Irker server. When
+configured, pushes to a project will trigger the service to send data directly
+to the Irker server.
+
+See the project homepage for further info: https://gitlab.com/esr/irker
+
+## Needed setup
+
+You will first need an Irker daemon. You can download the Irker code from its
+repository on https://gitlab.com/esr/irker:
+
+```
+git clone https://gitlab.com/esr/irker.git
+```
+
+Once you have downloaded the code, you can run the python script named `irkerd`.
+This script is the gateway script, it acts both as an IRC client, for sending
+messages to an IRC server obviously, and as a TCP server, for receiving messages
+from the GitLab service.
+
+If the Irker server runs on the same machine, you are done. If not, you will
+need to follow the firsts steps of the next section.
+
+## Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "Irker".
+1. Select the "Active" checkbox.
+1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
+in the `Server host` field on the Web page
+1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
+`Server port` field on the Web page.
+1. Optional: if `Default IRC URI` is set, it has to be in the format
+`irc[s]://domain.name` and will be prepend to each and every channel provided
+by the user which is not a full URI.
+1. Specify the recipients (e.g. #channel1, user1, etc.)
+1. Save or optionally click "Test Settings".
+
+## Note on Irker recipients
+
+Irker accepts channel names of the form `chan` and `#chan`, both for the
+`#chan` channel. If you want to send messages in query, you will need to add
+`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
+case, `Aorimn` is treated as a nick and no more as a channel name.
+
+Irker can also join password-protected channels. Users need to append
+`?key=thesecretpassword` to the chan name.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
new file mode 100644
index 00000000000..4c64d1e0907
--- /dev/null
+++ b/doc/user/project/integrations/jira.md
@@ -0,0 +1,209 @@
+# GitLab JIRA integration
+
+GitLab can be configured to interact with JIRA. Configuration happens via
+user name and password. Connecting to a JIRA server via CAS is not possible.
+
+Each project can be configured to connect to a different JIRA instance, see the
+[configuration](#configuration) section. If you have one JIRA instance you can
+pre-fill the settings page with a default template. To configure the template
+see the [Services Templates][services-templates] document.
+
+Once the project is connected to JIRA, you can reference and close the issues
+in JIRA directly from GitLab.
+
+## Configuration
+
+In order to enable the JIRA service in GitLab, you need to first configure the
+project in JIRA and then enter the correct values in GitLab.
+
+### Configuring JIRA
+
+We need to create a user in JIRA which will have access to all projects that
+need to integrate with GitLab. Login to your JIRA instance as admin and under
+Administration go to User Management and create a new user.
+
+As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
+group.
+
+**It is important that the user `GitLab` has write-access to projects in JIRA**
+
+We have split this stage in steps so it is easier to follow.
+
+---
+
+1. Login to your JIRA instance as an administrator and under **Administration**
+ go to **User Management** to create a new user.
+
+ ![JIRA user management link](img/jira_user_management_link.png)
+
+ ---
+
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
+ to projects in JIRA. Enter the user's name and a _valid_ e-mail address
+ since JIRA sends a verification e-mail to set-up the password.
+ _**Note:** JIRA creates the username automatically by using the e-mail
+ prefix. You can change it later if you want._
+
+ ![JIRA create new user](img/jira_create_new_user.png)
+
+ ---
+
+1. Now, let's create a `gitlab-developers` group which will have write access
+ to projects in JIRA. Go to the **Groups** tab and select **Create group**.
+
+ ![JIRA create new user](img/jira_create_new_group.png)
+
+ ---
+
+ Give it an optional description and hit **Create group**.
+
+ ![jira create new group](img/jira_create_new_group_name.png)
+
+ ---
+
+1. Give the newly-created group write access by going to
+ **Application access ➔ View configuration** and adding the `gitlab-developers`
+ group to JIRA Core.
+
+ ![JIRA group access](img/jira_group_access.png)
+
+ ---
+
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
+ **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers`
+ group from the dropdown menu. Notice that the group says _Access_ which is
+ what we aim for.
+
+ ![JIRA add user to group](img/jira_add_user_to_group.png)
+
+---
+
+The JIRA configuration is over. Write down the new JIRA username and its
+password as they will be needed when configuring GitLab in the next section.
+
+### Configuring GitLab
+
+>**Notes:**
+- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
+ higher is required.
+- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
+ the configuration options you have to enter. If you are using an older version,
+ [follow this documentation][jira-repo-old-docs].
+
+To enable JIRA integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **JIRA** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
+| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
+| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
+| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
+| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
+
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+
+![JIRA service page](img/jira_service_page.png)
+
+---
+
+## JIRA issues
+
+By now you should have [configured JIRA](#configuring-jira) and enabled the
+[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
+you should be able to reference and close JIRA issues by just mentioning their
+ID in GitLab commits and merge requests.
+
+### Referencing JIRA Issues
+
+When GitLab project has JIRA issue tracker configured and enabled, mentioning
+JIRA issue in GitLab will automatically add a comment in JIRA issue with the
+link back to GitLab. This means that in comments in merge requests and commits
+referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the
+format:
+
+```
+USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
+ENTITY_TITLE
+```
+
+* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
+* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
+* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
+* `PROJECT_NAME` GitLab project name.
+* `ENTITY_TITLE` Merge request title or commit message first line.
+
+![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
+
+---
+
+### Closing JIRA Issues
+
+JIRA issues can be closed directly from GitLab by using trigger words in
+commits and merge requests. When a commit which contains the trigger word
+followed by the JIRA issue ID in the commit message is pushed, GitLab will
+add a comment in the mentioned JIRA issue and immediately close it (provided
+the transition ID was set up correctly).
+
+There are currently three trigger words, and you can use either one to achieve
+the same goal:
+
+- `Resolves PROJECT-1`
+- `Closes PROJECT-1`
+- `Fixes PROJECT-1`
+
+where `PROJECT-1` is the issue ID of the JIRA project.
+
+### JIRA issue closing example
+
+Let's consider the following example:
+
+1. For the project named `PROJECT` in JIRA, we implemented a new feature
+ and created a merge request in GitLab.
+1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
+ in GitLab contains the improvement
+1. In the merge request description we use the issue closing trigger
+ `Closes PROJECT-7`.
+1. Once the merge request is merged, the JIRA issue will be automatically closed
+ with a comment and an associated link to the commit that resolved the issue.
+
+---
+
+In the following screenshot you can see what the link references to the JIRA
+issue look like.
+
+![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
+
+---
+
+Once this merge request is merged, the JIRA issue will be automatically closed
+with a link to the commit that resolved the issue.
+
+![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png)
+
+---
+
+![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
+
+## Troubleshooting
+
+If things don't work as expected that's usually because you have configured
+incorrectly the JIRA-GitLab integration.
+
+### GitLab is unable to comment on a ticket
+
+Make sure that the user you set up for GitLab to communicate with JIRA has the
+correct access permission to post comments on a ticket and to also transition
+the ticket, if you'd like GitLab to also take care of closing them.
+JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
+
+### GitLab is unable to close a ticket
+
+Make sure the `Transition ID` you set within the JIRA settings matches the one
+your project needs to close a ticket.
+
+[services-templates]: services_templates.md
+[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
new file mode 100644
index 00000000000..2a890acde4d
--- /dev/null
+++ b/doc/user/project/integrations/kubernetes.md
@@ -0,0 +1,67 @@
+# GitLab Kubernetes / OpenShift integration
+
+GitLab can be configured to interact with Kubernetes, or other systems using the
+Kubernetes API (such as OpenShift).
+
+Each project can be configured to connect to a different Kubernetes cluster, see
+the [configuration](#configuration) section.
+
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see the [Services Templates](services_templates.md) document.
+
+## Configuration
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+of your project and select the **Kubernetes** service to configure it.
+
+![Kubernetes configuration settings](img/kubernetes_configuration.png)
+
+The Kubernetes service takes the following arguments:
+
+1. Kubernetes namespace
+1. API URL
+1. Service token
+1. Custom CA bundle
+
+The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
+exposes several APIs - we want the "base" URL that is common to all of them,
+e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+GitLab authenticates against Kubernetes using service tokens, which are
+scoped to a particular `namespace`. If you don't have a service token yet,
+you can follow the
+[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
+to create one. You can also view or create service tokens in the
+[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
+`Config -> Secrets`.
+
+Fill in the service token and namespace according to the values you just got.
+If the API is using a self-signed TLS certificate, you'll also need to include
+the `ca.crt` contents as the `Custom CA bundle`.
+
+## Deployment variables
+
+The Kubernetes service exposes following
+[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+GitLab CI build environment:
+
+- `KUBE_URL` - equal to the API URL
+- `KUBE_TOKEN`
+- `KUBE_NAMESPACE`
+- `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
+
+>**NOTE:**
+Added in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is currently limited to the first container in the
+first pod of your environment.
+
+When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
+support to your environments. This is based on the `exec` functionality found in
+Docker and Kubernetes, so you get a new shell session within your existing
+containers. To use this integration, you should deploy to Kubernetes using
+the deployment variables above, ensuring any pods you create are labelled with
+`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
new file mode 100644
index 00000000000..cfb0931273d
--- /dev/null
+++ b/doc/user/project/integrations/mattermost.md
@@ -0,0 +1,47 @@
+# Mattermost Notifications Service
+
+## On Mattermost
+
+To enable Mattermost integration you must create an incoming webhook integration:
+
+1. Sign in to your Mattermost instance
+1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
+1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
+
+There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
+it on https://mattermost.example/admin_console/integrations/custom.
+
+Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
+
+## On GitLab
+
+After you set up Mattermost, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Mattermost notifications** service to configure it.
+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
+
+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 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/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md
new file mode 100644
index 00000000000..488f61c77a3
--- /dev/null
+++ b/doc/user/project/integrations/mattermost_slash_commands.md
@@ -0,0 +1,164 @@
+# Mattermost slash commands
+
+> Introduced in GitLab 8.14
+
+Mattermost commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in Mattermost.
+
+## Prerequisites
+
+Mattermost 3.4 and up is required.
+
+If you have the Omnibus GitLab package installed, Mattermost is already bundled
+in it. All you have to do is configure it. Read more in the
+[Omnibus GitLab Mattermost documentation][omnimmdocs].
+
+## Automated Configuration
+
+If Mattermost is installed on the same server as GitLab, the configuration process can be
+done for you by GitLab.
+
+Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
+
+## Manual Configuration
+
+The configuration consists of two parts. First you need to enable the slash
+commands in Mattermost and then enable the service in GitLab.
+
+### Step 1. Enable custom slash commands in Mattermost
+
+This step is only required when using a source install, omnibus installs will be
+preconfigured with the right settings.
+
+The first thing to do in Mattermost is to enable custom slash commands from
+the administrator console.
+
+1. Log in with an account that has admin privileges and navigate to the system
+ console.
+
+ ![Mattermost go to console](img/mattermost_goto_console.png)
+
+ ---
+
+1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
+ **Enable custom integrations to override usernames**, and **Override
+ custom integrations to override profile picture icons** to true
+
+ ![Mattermost console](img/mattermost_console_integrations.png)
+
+ ---
+
+1. Click **Save** at the bottom to save the changes.
+
+### Step 2. Open the Mattermost slash commands service in GitLab
+
+1. Open a new tab for GitLab, go to your project's
+ [Integrations page](project_services.md#accessing-the-project-services)
+ and select the **Mattermost command** service to configure it.
+ A screen will appear with all the values you need to copy in Mattermost as
+ described in the next step. Leave the window open.
+
+ >**Note:**
+ GitLab will propose some values for the Mattermost settings. The only one
+ required to copy-paste as-is is the **Request URL**, all the others are just
+ suggestions.
+
+ ![Mattermost setup instructions](img/mattermost_config_help.png)
+
+ ---
+
+1. Proceed to the next step and create a slash command in Mattermost with the
+ above values.
+
+### Step 3. Create a new custom slash command in Mattermost
+
+Now that you have enabled custom slash commands in Mattermost and opened
+the Mattermost slash commands service in GitLab, it's time to copy these values
+in a new slash command.
+
+1. Back to Mattermost, under your team page settings, you should see the
+ **Integrations** option.
+
+ ![Mattermost team integrations](img/mattermost_team_integrations.png)
+
+ ---
+
+1. Go to the **Slash Commands** integration and add a new one by clicking the
+ **Add Slash Command** button.
+
+ ![Mattermost add command](img/mattermost_add_slash_command.png)
+
+ ---
+
+1. Fill in the options for the custom command as described in
+ [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
+
+ >**Note:**
+ If you plan on connecting multiple projects, pick a slash command trigger
+ word that relates to your projects such as `/gitlab-project-name` or even
+ just `/project-name`. Only use `/gitlab` if you will only connect a single
+ project to your Mattermost team.
+
+ ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
+
+1. After you setup all the values, copy the token (we will use it below) and
+ click **Done**.
+
+ ![Mattermost slash command token](img/mattermost_slash_command_token.png)
+
+### Step 4. Copy the Mattermost token into the Mattermost slash command service
+
+1. In GitLab, paste the Mattermost token you copied in the previous step and
+ check the **Active** checkbox.
+
+ ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
+
+1. Click **Save changes** for the changes to take effect.
+
+---
+
+You are now set to start using slash commands in Mattermost that talk to the
+GitLab project you configured.
+
+## Authorizing Mattermost to interact with GitLab
+
+The first time a user will interact with the newly created slash commands,
+Mattermost will trigger an authorization process.
+
+![Mattermost bot authorize](img/mattermost_bot_auth.png)
+
+This will connect your Mattermost user with your GitLab user. You can
+see all authorized chat accounts in your profile's page under **Chat**.
+
+When the authorization process is complete, you can start interacting with
+GitLab using the Mattermost commands.
+
+## Available slash commands
+
+The available slash commands are:
+
+| Command | Description | Example |
+| ------- | ----------- | ------- |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
+
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
+
+![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
+
+## Permissions
+
+The permissions to run the [available commands](#available-slash-commands) derive from
+the [permissions you have on the project](../../permissions.md#project).
+
+## Further reading
+
+- [Mattermost slash commands documentation][mmslashdocs]
+- [Omnibus GitLab Mattermost][omnimmdocs]
+
+[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
+[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
+[ciyaml]: ../../../ci/yaml/README.md
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
new file mode 100644
index 00000000000..a3a163a4c6b
--- /dev/null
+++ b/doc/user/project/integrations/project_services.md
@@ -0,0 +1,76 @@
+# Project services
+
+Project services allow you to integrate GitLab with other applications. They
+are a bit like plugins in that they allow a lot of freedom in adding
+functionality to GitLab.
+
+## Accessing the project services
+
+You can find the available services under the **Integrations** page in your
+project's settings.
+
+1. Navigate to the cog icon in the upper right corner of your project. You need
+ to have at least [master permission][permissions] on the project.
+
+ ![Accessing the services](img/accessing_integrations.png)
+
+1. There are more than 20 services to integrate with. Click on the one that you
+ want to configure.
+
+ ![Project services list](img/project_services.png)
+
+Below, you will find a list of the currently supported ones accompanied with
+comprehensive documentation.
+
+## Services
+
+Click on the service links to see further configuration instructions and details.
+
+| Service | Description |
+| ------- | ----------- |
+| Asana | Asana - Teamwork without email |
+| 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 |
+| Drone CI | Continuous Integration platform built on Docker, written in Go |
+| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
+| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
+| Flowdock | Flowdock is a collaboration web app for technical teams |
+| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| [HipChat](hipchat.md) | Private group chat and IM |
+| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
+| [JIRA](jira.md) | JIRA issue tracker |
+| JetBrains TeamCity CI | A continuous integration and build server |
+| [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 |
+| [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) |
+| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
+| [Redmine](redmine.md) | Redmine issue tracker |
+
+## Services templates
+
+Services templates is a way to set some predefined values in the Service of
+your liking which will then be pre-filled on each project's Service.
+
+Read more about [Services templates in this document](services_templates.md).
+
+## Contributing to project services
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available, please see the
+[project_services source directory][projects-code].
+
+Contributions are welcome!
+
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
new file mode 100644
index 00000000000..89c0312d3c2
--- /dev/null
+++ b/doc/user/project/integrations/redmine.md
@@ -0,0 +1,23 @@
+# Redmine Service
+
+To enable the Redmine integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Redmine** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
+| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
+
+Once you have configured and enabled Redmine:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Redmine issue index
+- clicking **New issue** on the project dashboard creates a new Redmine issue
+
+As an example, below is a configuration for a project named gitlab-ci.
+
+![Redmine configuration](img/redmine_configuration.png)
diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md
new file mode 100644
index 00000000000..5b04d7d88b8
--- /dev/null
+++ b/doc/user/project/integrations/services_templates.md
@@ -0,0 +1,26 @@
+# Services templates
+
+A GitLab administrator can add a service template that sets a default for each
+project. After a service template is enabled, it will be applied to new
+projects only and its details will be pre-filled on the project's Service page.
+
+## Enable a service template
+
+In GitLab's Admin area, navigate to **Service Templates** and choose the
+service template you wish to create.
+
+## Services for external issue trackers
+
+In the image below you can see how a service template for Redmine would look
+like.
+
+![Redmine service template](img/services_templates_redmine_example.png)
+
+---
+
+For each project, you will still need to configure the issue tracking
+URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
+by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
+the project settings, and GitLab would automatically update the URL configured
+in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
+must be configured directly within the project's **Integrations** settings.
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
new file mode 100644
index 00000000000..f27f9a726fc
--- /dev/null
+++ b/doc/user/project/integrations/slack.md
@@ -0,0 +1,53 @@
+# Slack Notifications Service
+
+## On Slack
+
+To enable Slack integration you must create an incoming webhook integration on
+Slack:
+
+1. [Sign in to Slack](https://slack.com/signin)
+1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
+1. Choose the channel name you want to send notifications to.
+1. Click **Add Incoming WebHooks Integration**
+1. Copy the **Webhook URL**, we'll need this later for GitLab.
+
+## On GitLab
+
+After you set up Slack, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack notifications** service to configure it.
+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
+
+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. |
+| **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.
+
+>**Note:**
+You can set "branch,pushed,Compare changes" as highlight words on your Slack
+profile settings, so that you can be aware of new commits when somebody pushes
+them.
+
+![Slack configuration](img/slack_configuration.png)
+
+[slackhook]: https://my.slack.com/services/new/incoming-webhook
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
new file mode 100644
index 00000000000..56f1ba7311e
--- /dev/null
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -0,0 +1,24 @@
+# Slack slash commands
+
+> Introduced in GitLab 8.15
+
+Slack commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in chat.
+For all available commands try the help subcommand, for example: `/gitlab help`,
+all review the [full list of commands](../../../integration/chat_commands.md).
+
+## Prerequisites
+
+A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
+Slack should be created beforehand, GitLab cannot create it for you.
+
+## Configuration
+
+Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack slash commands** service to configure it.
+
+![Slack setup instructions](img/slack_setup.png)
+
+Once you've followed the instructions, mark the service as active and insert the token
+you've received from Slack. After saving the service you are good to go!
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
new file mode 100644
index 00000000000..ed1e867f5fb
--- /dev/null
+++ b/doc/user/project/integrations/webhooks.md
@@ -0,0 +1,1028 @@
+# Webhooks
+
+>**Note:**
+Starting from GitLab 8.5:
+- the `repository` key is deprecated in favor of the `project` key
+- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
+- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+Webhooks can be used to update an external issue tracker, trigger CI jobs,
+update a backup mirror, or even deploy to your production server.
+
+Navigate to the webhooks page by going to the **Integrations** page from your
+project's settings which can be found under the wheel icon in the upper right
+corner.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Webhook endpoint tips
+
+If you are writing your own endpoint (web server) that will receive
+GitLab webhooks keep in mind the following things:
+
+- Your endpoint should send its HTTP response as fast as possible. If
+ you wait too long, GitLab may decide the hook failed and retry it.
+- Your endpoint should ALWAYS return a valid HTTP response. If you do
+ not do this then GitLab will think the hook failed and retry it.
+ Most HTTP libraries take care of this for you automatically but if
+ you are writing a low-level hook this is important to remember.
+- GitLab ignores the HTTP status code returned by your endpoint.
+
+## Secret token
+
+If you specify a secret token, it will be sent with the hook request in the
+`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
+that the request is legitimate.
+
+## SSL verification
+
+By default, the SSL certificate of the webhook endpoint is verified based on
+an internal list of Certificate Authorities, which means the certificate cannot
+be self-signed.
+
+You can turn this off in the webhook settings in your GitLab projects.
+
+![SSL Verification](img/webhooks_ssl.png)
+
+## Events
+
+Below are described the supported events.
+
+### Push events
+
+Triggered when you push to the repository except when pushing tags.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "push",
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "user_id": 4,
+ "user_name": "John Smith",
+ "user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 15,
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "ssh_url":"git@example.com:mike/diaspora.git",
+ "http_url":"http://example.com/mike/diaspora.git"
+ },
+ "repository":{
+ "name": "Diaspora",
+ "url": "git@example.com:mike/diaspora.git",
+ "description": "",
+ "homepage": "http://example.com/mike/diaspora",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "visibility_level":0
+ },
+ "commits": [
+ {
+ "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "message": "Update Catalan translation to e38cb41.",
+ "timestamp": "2011-12-12T14:27:31+02:00",
+ "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "author": {
+ "name": "Jordi Mallach",
+ "email": "jordi@softcatala.org"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ },
+ {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ }
+ ],
+ "total_commits_count": 4
+}
+```
+
+### Tag events
+
+Triggered when you create (or delete) tags to the repository.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Tag Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "tag_push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
+ "name": "Example",
+ "url": "ssh://git@example.com/jsmith/example.git",
+ "description": "",
+ "homepage": "http://example.com/jsmith/example",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
+
+### Issues events
+
+Triggered when a new issue is created or an existing issue was updated/closed/reopened.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Issue Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "issue",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 301,
+ "title": "New API: create/update/delete file",
+ "assignee_id": 51,
+ "author_id": 51,
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Create new API for manipulations with repository",
+ "milestone_id": null,
+ "state": "opened",
+ "iid": 23,
+ "url": "http://example.com/diaspora/issues/23",
+ "action": "open"
+ },
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+}
+```
+### Comment events
+
+Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
+The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
+payload will also include information about the target of the comment. For example,
+a comment on a issue will include the specific issue information under the `issue` key.
+Valid target types:
+
+1. `commit`
+2. `merge_request`
+3. `issue`
+4. `snippet`
+
+#### Comment on commit
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1243,
+ "note": "This is a commit comment. How does this work?",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:08:09 UTC",
+ "updated_at": "2015-05-17 18:08:09 UTC",
+ "project_id": 5,
+ "attachment":null,
+ "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
+ "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "noteable_id": null,
+ "system": false,
+ "st_diff": {
+ "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
+ "new_path": "six",
+ "old_path": "six",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ },
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
+ },
+ "commit": {
+ "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "timestamp": "2014-02-27T10:06:20+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+}
+```
+
+#### Comment on merge request
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://localhost/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1244,
+ "note": "This MR needs work.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:21:36 UTC",
+ "updated_at": "2015-05-17 18:21:36 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 7,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
+ },
+ "merge_request": {
+ "id": 7,
+ "target_branch": "markdown",
+ "source_branch": "master",
+ "source_project_id": 5,
+ "author_id": 8,
+ "assignee_id": 28,
+ "title": "Tempora et eos debitis quae laborum et.",
+ "created_at": "2015-03-01 20:12:53 UTC",
+ "updated_at": "2015-03-21 18:27:27 UTC",
+ "milestone_id": 11,
+ "state": "opened",
+ "merge_status": "cannot_be_merged",
+ "target_project_id": 5,
+ "iid": 1,
+ "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
+ "position": 0,
+ "locked_at": null,
+ "source":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "target": {
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "last_commit": {
+ "id": "562e173be03b8ff2efb05345d12df18815438a4b",
+ "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
+ "timestamp": "2015-04-08T21: 00:25-07:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
+ "author": {
+ "name": "John Smith",
+ "email": "john@example.com"
+ }
+ },
+ "work_in_progress": false,
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+#### Comment on issue
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "description":"",
+ "homepage":"http://example.com/mike/diaspora"
+ },
+ "object_attributes": {
+ "id": 1241,
+ "note": "Hello world",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2015-05-17 17:06:40 UTC",
+ "updated_at": "2015-05-17 17:06:40 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 92,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
+ },
+ "issue": {
+ "id": 92,
+ "title": "test",
+ "assignee_id": null,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-12 14:53:17 UTC",
+ "updated_at": "2015-04-26 08:28:42 UTC",
+ "position": 0,
+ "branch_name": null,
+ "description": "test",
+ "milestone_id": null,
+ "state": "closed",
+ "iid": 17
+ }
+}
+```
+
+#### Comment on code snippet
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"Gitlab Test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "description":"Aut reprehenderit ut est.",
+ "homepage":"http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1245,
+ "note": "Is this snippet doing what it's supposed to be doing?",
+ "noteable_type": "Snippet",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:35:50 UTC",
+ "updated_at": "2015-05-17 18:35:50 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 53,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
+ },
+ "snippet": {
+ "id": 53,
+ "title": "test",
+ "content": "puts 'Hello world'",
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-09 02:40:38 UTC",
+ "updated_at": "2015-04-09 02:40:38 UTC",
+ "file_name": "test.rb",
+ "expires_at": null,
+ "type": "ProjectSnippet",
+ "visibility_level": 0
+ }
+}
+```
+
+### Merge request events
+
+Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Merge Request Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "merge_request",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "object_attributes": {
+ "id": 99,
+ "target_branch": "master",
+ "source_branch": "ms-viewport",
+ "source_project_id": 14,
+ "author_id": 51,
+ "assignee_id": 6,
+ "title": "MS-Viewport",
+ "created_at": "2013-12-03T17:23:34Z",
+ "updated_at": "2013-12-03T17:23:34Z",
+ "st_commits": null,
+ "st_diffs": null,
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 14,
+ "iid": 1,
+ "description": "",
+ "source":{
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "target": {
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "last_commit": {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ }
+ },
+ "work_in_progress": false,
+ "url": "http://example.com/diaspora/merge_requests/1",
+ "action": "open",
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+### Wiki Page events
+
+Triggered when a wiki page is created, edited or deleted.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Wiki Page Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "wiki_page",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
+ },
+ "project": {
+ "name": "awesome-project",
+ "description": "This is awesome",
+ "web_url": "http://example.com/root/awesome-project",
+ "avatar_url": null,
+ "git_ssh_url": "git@example.com:root/awesome-project.git",
+ "git_http_url": "http://example.com/root/awesome-project.git",
+ "namespace": "root",
+ "visibility_level": 0,
+ "path_with_namespace": "root/awesome-project",
+ "default_branch": "master",
+ "homepage": "http://example.com/root/awesome-project",
+ "url": "git@example.com:root/awesome-project.git",
+ "ssh_url": "git@example.com:root/awesome-project.git",
+ "http_url": "http://example.com/root/awesome-project.git"
+ },
+ "wiki": {
+ "web_url": "http://example.com/root/awesome-project/wikis/home",
+ "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
+ "git_http_url": "http://example.com/root/awesome-project.wiki.git",
+ "path_with_namespace": "root/awesome-project.wiki",
+ "default_branch": "master"
+ },
+ "object_attributes": {
+ "title": "Awesome",
+ "content": "awesome content goes here",
+ "format": "markdown",
+ "message": "adding an awesome page to the wiki",
+ "slug": "awesome",
+ "url": "http://example.com/root/awesome-project/wikis/awesome",
+ "action": "create"
+ }
+}
+```
+
+### Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "pipeline",
+ "object_attributes":{
+ "id": 31,
+ "ref": "master",
+ "tag": false,
+ "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "status": "success",
+ "stages":[
+ "build",
+ "test",
+ "deploy"
+ ],
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "duration": 63
+ },
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "project":{
+ "name": "Gitlab Test",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "avatar_url": null,
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "namespace": "Gitlab Org",
+ "visibility_level": 20,
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "default_branch": "master"
+ },
+ "commit":{
+ "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "message": "test\n",
+ "timestamp": "2016-08-12T17:23:21+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "author":{
+ "name": "User",
+ "email": "user@gitlab.com"
+ }
+ },
+ "builds":[
+ {
+ "id": 380,
+ "stage": "deploy",
+ "name": "production",
+ "status": "skipped",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "manual",
+ "manual": true,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 377,
+ "stage": "test",
+ "name": "test-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 378,
+ "stage": "test",
+ "name": "test-build",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 376,
+ "stage": "build",
+ "name": "build-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:24:56 UTC",
+ "finished_at": "2016-08-12 15:25:26 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 379,
+ "stage": "deploy",
+ "name": "staging",
+ "status": "created",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ }
+ ]
+}
+```
+
+### Build events
+
+Triggered on status change of a Build.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Build Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "build",
+ "ref": "gitlab-script-trigger",
+ "tag": false,
+ "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "build_id": 1977,
+ "build_name": "test",
+ "build_stage": "test",
+ "build_status": "created",
+ "build_started_at": null,
+ "build_finished_at": null,
+ "build_duration": null,
+ "build_allow_failure": false,
+ "project_id": 380,
+ "project_name": "gitlab-org/gitlab-test",
+ "user": {
+ "id": 3,
+ "name": "User",
+ "email": "user@gitlab.com"
+ },
+ "commit": {
+ "id": 2366,
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "message": "test\n",
+ "author_name": "User",
+ "author_email": "user@gitlab.com",
+ "status": "created",
+ "duration": null,
+ "started_at": null,
+ "finished_at": null
+ },
+ "repository": {
+ "name": "gitlab_test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "visibility_level": 20
+ }
+}
+```
+
+## Example webhook receiver
+
+If you want to see GitLab's webhooks in action for testing purposes you can use
+a simple echo script running in a console session. For the following script to
+work you need to have Ruby installed.
+
+Save the following file as `print_http_body.rb`:
+
+```ruby
+require 'webrick'
+
+server = WEBrick::HTTPServer.new(:Port => ARGV.first)
+server.mount_proc '/' do |req, res|
+ puts req.body
+end
+
+trap 'INT' do
+ server.shutdown
+end
+server.start
+```
+
+Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
+8000`. Then add your server as a webhook receiver in GitLab as
+`http://my.host:8000/`.
+
+When you press 'Test Hook' in GitLab, you should see something like this in the
+console:
+
+```
+{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
+example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
+- -> /
+```
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index d1ae57c00d7..3199d370a58 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,6 +1,8 @@
# Issue board
-> [Introduced][ce-5554] in GitLab 8.11.
+>**Notes:**
+- [Introduced][ce-5554] in GitLab 8.11.
+- The Backlog column was replaced by the **Add issues** button in GitLab 8.17.
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board.
| **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). |
-There are three types of lists, the ones you create based on your labels, and
-two default:
+There are two types of lists, the ones you create based on your labels, and
+one default:
-- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
-- **Done** (default): shows all closed issues. Always appears on the very right.
-Label list: a list based on a label. It shows all issues with that label.
- Label list: a list based on a label. It shows all opened issues with that label.
+- **Done** (default): shows all closed issues. Always appears on the very right.
![GitLab Issue Board](img/issue_board.png)
@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board:
If you are not able to perform one or more of the things above, make sure you
have the right [permissions](#permissions).
-## First time using the Issue Board
+## First time using the issue board
-The first time you navigate to your Issue Board, you will be presented with the
-two default lists (**Backlog** and **Done**) and a welcoming message that gives
+The first time you navigate to your Issue Board, you will be presented with
+a default list (**Done**) and a welcoming message that gives
you two options. You can either create a predefined set of labels and create
their corresponding lists to the Issue Board or opt-out and use your own lists.
@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm.
Deleting a list doesn't have any effect in issues and labels, it's just the
list view that is removed. You can always add it back later if you need.
-## Searching issues in the Backlog list
+## Adding issues to a list
+
+You can add issues to a list by clicking the **Add issues** button that is
+present in the upper right corner of the issue board. This will open up a modal
+window where you can see all the issues that do not belong to any list.
+
+Select one or more issues by clicking on the cards and then click **Add issues**
+to add them to the selected list. You can limit the issues you want to add to
+the list by filtering by author, assignee, milestone and label.
-The very first time you start using the Issue Board, it is very likely your
-issue tracker is already populated with labels and issues. In that case,
-**Backlog** will have all the issues that don't belong to another list, and
-**Done** will have all the closed ones.
+![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png)
-For performance and visibility reasons, each list shows the first 20 issues
-by default. If you have more than 20, you have to start scrolling down for the
-next 20 issues to appear. This can be cumbersome if your issue tracker hosts
-hundreds of issues, and for that reason it is easier to search for issues to
-move from **Backlog** to another list.
+## Removing an issue from a list
-Start typing in the search bar under the **Backlog** list and the relevant
-issues will appear.
+Removing an issue from a list can be done by clicking on the issue card and then
+clicking the **Remove from board** button in the sidebar. Under the hood, the
+respective label is removed, and as such it's also removed from the list and the
+board itself.
-![Issue Board search Backlog](img/issue_board_search_backlog.png)
+![Remove issue from list](img/issue_boards_remove_issue.png)
## Filtering issues
@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be:
and gets automatically closed.
For instance you can create a list based on the label of 'Frontend' and one for
-'Backend'. A designer can start working on an issue by dragging it from
-**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being
+'Backend'. A designer can start working on an issue by adding it to the
+'Frontend' list. That way, everyone knows that this issue is now being
worked on by the designers. Then, once they're done, all they have to do is
drag it over to the next list, 'Backend', where a backend developer can
eventually pick it up. Once they’re done, they move it to **Done**, to close the
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
index be09337319f..84a79f04094 100644
--- a/doc/user/project/merge_requests.md
+++ b/doc/user/project/merge_requests.md
@@ -1,169 +1 @@
-# Merge Requests
-
-Merge requests allow you to exchange changes you made to source code and
-collaborate with other people on the same project.
-
-## Authorization for merge requests
-
-There are two main ways to have a merge request flow with GitLab:
-
-1. Working with [protected branches][] in a single repository
-1. Working with forks of an authoritative project
-
-[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
-
-## Cherry-pick changes
-
-Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
-in a merged merge requests or a commit.
-
-[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
-
-## Merge when pipeline succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or
-more CI builds running, you can set it to be merged automatically when CI
-pipeline succeeds. This way, you don't have to wait for the pipeline to finish
-and remember to merge the request manually.
-
-[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md)
-
-## Resolve discussion comments in merge requests reviews
-
-Keep track of the progress during a code review with resolving comments.
-Resolving comments prevents you from forgetting to address feedback and lets
-you hide discussions that are no longer relevant.
-
-[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
-
-## Resolve conflicts
-
-When a merge request has conflicts, GitLab may provide the option to resolve
-those conflicts in the GitLab UI.
-
-[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
-
-## Revert changes
-
-GitLab implements Git's powerful feature to revert any commit with introducing
-a **Revert** button in merge requests and commit details.
-
-[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
-
-## Merge requests versions
-
-Every time you push to a branch that is tied to a merge request, a new version
-of merge request diff is created. When you visit a merge request that contains
-more than one pushes, you can select and compare the versions of those merge
-request diffs.
-
-[Read more about the merge requests versions.](merge_requests/versions.md)
-
-## Work In Progress merge requests
-
-To prevent merge requests from accidentally being accepted before they're
-completely ready, GitLab blocks the "Accept" button for merge requests that
-have been marked as a **Work In Progress**.
-
-[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
-
-## Ignore whitespace changes in Merge Request diff view
-
-If you click the **Hide whitespace changes** button, you can see the diff
-without whitespace changes (if there are any). This is also working when on a
-specific commit page.
-
-![MR diff](merge_requests/img/merge_request_diff.png)
-
->**Tip:**
-You can append `?w=1` while on the diffs page of a merge request to ignore any
-whitespace changes.
-
-## Tips
-
-Here are some tips that will help you be more efficient with merge requests in
-the command line.
-
-> **Note:**
-This section might move in its own document in the future.
-
-### Checkout merge requests locally
-
-A merge request contains all the history from a repository, plus the additional
-commits added to the branch associated with the merge request. Here's a few
-tricks to checkout a merge request locally.
-
-Please note that you can checkout a merge request locally even if the source
-project is a fork (even a private fork) of the target project.
-
-#### Checkout locally by adding a git alias
-
-Add the following alias to your `~/.gitconfig`:
-
-```
-[alias]
- mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
-```
-
-Now you can check out a particular merge request from any repository and any
-remote. For example, to check out the merge request with ID 5 as shown in GitLab
-from the `upstream` remote, do:
-
-```
-git mr upstream 5
-```
-
-This will fetch the merge request into a local `mr-upstream-5` branch and check
-it out.
-
-#### Checkout locally by modifying `.git/config` for a given repository
-
-Locate the section for your GitLab remote in the `.git/config` file. It looks
-like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-You can open the file with:
-
-```
-git config -e
-```
-
-Now add the following line to the above section:
-
-```
-fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-In the end, it should look like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
- fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests:
-
-```
-git fetch origin
-
-...
-From https://gitlab.com/gitlab-org/gitlab-ce.git
- * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-And to check out a particular merge request:
-
-```
-git checkout origin/merge-requests/1
-```
-
-[protected branches]: protected_branches.md
+This document was moved to [merge_requests/index.md](merge_requests/index.md).
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/index.md b/doc/user/project/merge_requests/index.md
new file mode 100644
index 00000000000..c759b7aaa4a
--- /dev/null
+++ b/doc/user/project/merge_requests/index.md
@@ -0,0 +1,169 @@
+# Merge requests
+
+Merge requests allow you to exchange changes you made to source code and
+collaborate with other people on the same project.
+
+## Authorization for merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches][] in a single repository
+1. Working with forks of an authoritative project
+
+[Learn more about the authorization for merge requests.](authorization_for_merge_requests.md)
+
+## Cherry-pick changes
+
+Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
+in a merged merge requests or a commit.
+
+[Learn more about cherry-picking changes.](cherry_pick_changes.md)
+
+## Merge when pipeline succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI jobs running, you can set it to be merged automatically when CI
+pipeline succeeds. This way, you don't have to wait for the pipeline to finish
+and remember to merge the request manually.
+
+[Learn more about merging when pipeline succeeds.](merge_when_pipeline_succeeds.md)
+
+## Resolve discussion comments in merge requests reviews
+
+Keep track of the progress during a code review with resolving comments.
+Resolving comments prevents you from forgetting to address feedback and lets
+you hide discussions that are no longer relevant.
+
+[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md)
+
+## Resolve conflicts
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI.
+
+[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
+
+## Revert changes
+
+GitLab implements Git's powerful feature to revert any commit with introducing
+a **Revert** button in merge requests and commit details.
+
+[Learn more about reverting changes in the UI](revert_changes.md)
+
+## Merge requests versions
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+[Read more about the merge requests versions.](versions.md)
+
+## Work In Progress merge requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked as a **Work In Progress**.
+
+[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
+
+## Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
+#### Checkout locally by adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any
+remote. For example, to check out the merge request with ID 5 as shown in GitLab
+from the `upstream` remote, do:
+
+```
+git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check
+it out.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-ce.git
+ * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+[protected branches]: ../protected_branches.md
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 75ad18b28cf..bdd7d0022e6 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -1,11 +1,11 @@
# Merge When Pipeline Succeeds
When reviewing a merge request that looks ready to merge but still has one or
-more CI builds running, you can set it to be merged automatically when the
-builds pipeline succeeds. This way, you don't have to wait for the builds to
+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,13 +16,13 @@ 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 builds,
+When the pipeline fails, the author gets a chance to retry any failed jobs,
or to push new commits to fix the failure.
-When the builds are retried and succeed on the second try, the merge request
+When the jobs are retried and succeed on the second try, the merge request
will automatically be merged after all. When the merge request is updated with
new commits, the automatic merge is automatically canceled to allow the new
changes to be reviewed.
@@ -30,17 +30,18 @@ changes to be reviewed.
## Only allow merge requests to be merged if the pipeline succeeds
> **Note:**
-You need to have builds configured to enable this feature.
+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 builds pass.
+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/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 77eab7ba5e3..610250ccf12 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -1,6 +1,12 @@
# Merge requests versions
-> Will be [introduced][ce-5467] in GitLab 8.12.
+>**Notes:**
+- [Introduced][ce-5467] in GitLab 8.12.
+- Comments are disabled while viewing outdated merge versions or comparing to
+ versions other than base.
+- Merge request versions are based on push not on commit. So, if you pushed 5
+ commits in a single push, it will be a single option in the dropdown. If you
+ pushed 5 times, that will count for 5 options.
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
@@ -30,13 +36,4 @@ changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png)
----
-
->**Notes:**
-- Comments are disabled while viewing outdated merge versions or comparing to
- versions other than base.
-- Merge request versions are based on push not on commit. So, if you pushed 5
- commits in a single push, it will be a single option in the dropdown. If you
- pushed 5 times, that will count for 5 options.
-
[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 320faff65c5..5f631f63050 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -1,55 +1,55 @@
-# New CI build permissions model
+# New CI job permissions model
> Introduced in GitLab 8.12.
-GitLab 8.12 has a completely redesigned [build permissions] system. You can find
+GitLab 8.12 has a completely redesigned [job permissions] system. You can find
all discussion and all our concerns when choosing the current approach in issue
[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994).
---
-Builds permissions should be tightly integrated with the permissions of a user
-who is triggering a build.
+Jobs permissions should be tightly integrated with the permissions of a user
+who is triggering a job.
The reasons to do it like that are:
- We already have a permissions system in place: group and project membership
of users.
-- We already fully know who is triggering a build (using `git push`, using the
+- We already fully know who is triggering a job (using `git push`, using the
web UI, executing triggers).
- We already know what user is allowed to do.
-- We use the user permissions for builds that are triggered by the user.
+- We use the user permissions for jobs that are triggered by the user.
- It opens a lot of possibilities to further enforce user permissions, like
allowing only specific users to access runners or use secure variables and
environments.
-- It is simple and convenient that your build can access everything that you
+- It is simple and convenient that your job can access everything that you
as a user have access to.
-- Short living unique tokens are now used, granting access for time of the build
+- Short living unique tokens are now used, granting access for time of the job
and maximizing security.
-With the new behavior, any build that is triggered by the user, is also marked
+With the new behavior, any job that is triggered by the user, is also marked
with their permissions. When a user does a `git push` or changes files through
the web UI, a new pipeline will be usually created. This pipeline will be marked
-as created be the pusher (local push or via the UI) and any build created in this
+as created be the pusher (local push or via the UI) and any job created in this
pipeline will have the permissions of the pusher.
This allows us to make it really easy to evaluate the access for all projects
that have [Git submodules][gitsub] or are using container images that the pusher
-would have access too. **The permission is granted only for time that build is
-running. The access is revoked after the build is finished.**
+would have access too. **The permission is granted only for time that job is
+running. The access is revoked after the job is finished.**
## Types of users
It is important to note that we have a few types of users:
-- **Administrators**: CI builds created by Administrators will not have access
+- **Administrators**: CI jobs created by Administrators will not have access
to all GitLab projects, but only to projects and container images of projects
that the administrator is a member of.That means that if a project is either
public or internal users have access anyway, but if a project is private, the
Administrator will have to be a member of it in order to have access to it
- via another project's build.
+ via another project's job.
-- **External users**: CI builds created by [external users][ext] will have
+- **External users**: CI jobs created by [external users][ext] will have
access only to projects to which user has at least reporter access. This
rules out accessing all internal projects by default,
@@ -57,46 +57,46 @@ This allows us to make the CI and permission system more trustworthy.
Let's consider the following scenario:
1. You are an employee of a company. Your company has a number of internal tools
- hosted in private repositories and you have multiple CI builds that make use
+ hosted in private repositories and you have multiple CI jobs that make use
of these repositories.
-2. You invite a new [external user][ext]. CI builds created by that user do not
+2. You invite a new [external user][ext]. CI jobs created by that user do not
have access to internal repositories, because the user also doesn't have the
access from within GitLab. You as an employee have to grant explicit access
for this user. This allows us to prevent from accidental data leakage.
-## Build token
+## Job token
-A unique build token is generated for each build and it allows the user to
+A unique job token is generated for each job and it allows the user to
access all projects that would be normally accessible to the user creating that
-build.
+job.
We try to make sure that this token doesn't leak by:
-1. Securing all API endpoints to not expose the build token.
-1. Masking the build token from build logs.
-1. Allowing to use the build token **only** when build is running.
+1. Securing all API endpoints to not expose the job token.
+1. Masking the job token from job logs.
+1. Allowing to use the job token **only** when job is running.
However, this brings a question about the Runners security. To make sure that
this token doesn't leak, you should also make sure that you configure
your Runners in the most possible secure way, by avoiding the following:
1. Any usage of Docker's `privileged` mode is risky if the machines are re-used.
-1. Using the `shell` executor since builds run on the same machine.
+1. Using the `shell` executor since jobs run on the same machine.
By using an insecure GitLab Runner configuration, you allow the rogue developers
-to steal the tokens of other builds.
+to steal the tokens of other jobs.
-## Build triggers
+## job triggers
-[Build triggers][triggers] do not support the new permission model.
-They continue to use the old authentication mechanism where the CI build
+[job triggers][triggers] do not support the new permission model.
+They continue to use the old authentication mechanism where the CI job
can access only its own sources. We plan to remove that limitation in one of
the upcoming releases.
## Before GitLab 8.12
-In versions before GitLab 8.12, all CI builds would use the CI Runner's token
+In versions before GitLab 8.12, all CI jobs would use the CI Runner's token
to checkout project sources.
The project's Runner's token was a token that you could find under the
@@ -105,7 +105,7 @@ project.
It could be used for registering new specific Runners assigned to the project
and to checkout project sources.
It could also be used with the GitLab Container Registry for that project,
-allowing pulling and pushing Docker images from within the CI build.
+allowing pulling and pushing Docker images from within the CI job.
---
@@ -115,7 +115,7 @@ GitLab would create a special checkout URL like:
https://gitlab-ci-token:<project-runners-token>/gitlab.com/gitlab-org/gitlab-ce.git
```
-And then the users could also use it in their CI builds all Docker related
+And then the users could also use it in their CI jobs all Docker related
commands to interact with GitLab Container Registry. For example:
```
@@ -125,7 +125,7 @@ docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
Using single token had multiple security implications:
- The token would be readable to anyone who had developer access to a project
- that could run CI builds, allowing the developer to register any specific
+ that could run CI jobs, allowing the developer to register any specific
Runner for that project.
- The token would allow to access only the project's sources, forbidding from
accessing any other projects.
@@ -133,12 +133,12 @@ Using single token had multiple security implications:
for registering specific runners and for accessing a project's container
registry with read-write permissions.
-All the above led to a new permission model for builds that was introduced
+All the above led to a new permission model for jobs that was introduced
with GitLab 8.12.
-## Making use of the new CI build permissions model
+## Making use of the new CI job permissions model
-With the new build permissions model, there is now an easy way to access all
+With the new job permissions model, there is now an easy way to access all
dependent source code in a project. That way, we can:
1. Access a project's [Git submodules][gitsub]
@@ -151,9 +151,9 @@ the container registry.
### Prerequisites to use the new permissions model
-With the new permissions model in place, there may be times that your build will
+With the new permissions model in place, there may be times that your job will
fail. This is most likely because your project tries to access other project's
-sources, and you don't have the appropriate permissions. In the build log look
+sources, and you don't have the appropriate permissions. In the job log look
for information about 403 or forbidden access messages.
In short here's what you need to do should you encounter any issues.
@@ -175,7 +175,7 @@ As a user:
- Make sure you are a member of the group or project you're trying to have
access to. As an Administrator, you can verify that by impersonating the user
- and retry the failing build in order to verify that everything is correct.
+ and retry the failing job in order to verify that everything is correct.
### Git submodules
@@ -199,9 +199,9 @@ Container Registries for private projects.
to pass a personal access token instead of your password in order to login to
GitLab's Container Registry.
-Your builds can access all container images that you would normally have access
+Your jobs can access all container images that you would normally have access
to. The only implication is that you can push to the Container Registry of the
-project for which the build is triggered.
+project for which the job is triggered.
This is how an example usage can look like:
@@ -213,7 +213,7 @@ test:
- docker run $CI_REGISTRY/group/other-project:latest
```
-[build permissions]: ../permissions.md#builds-permissions
+[job permissions]: ../permissions.md#jobs-permissions
[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
[gitsub]: ../../ci/git_submodules.md
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
new file mode 100644
index 00000000000..be47f9d2a44
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..2f1a19ae424
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..274e98fde4d
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..6bc92db6b41
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..cd59defa006
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..b064310380e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..dc431ea3fef
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..abe6b4cbd8e
--- /dev/null
+++ b/doc/user/project/pages/index.md
@@ -0,0 +1,49 @@
+# 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/pipelines/img/job_artifacts_browser.png b/doc/user/project/pipelines/img/job_artifacts_browser.png
new file mode 100644
index 00000000000..145fe156bbb
--- /dev/null
+++ b/doc/user/project/pipelines/img/job_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/job_artifacts_browser_button.png b/doc/user/project/pipelines/img/job_artifacts_browser_button.png
new file mode 100644
index 00000000000..21072ce1248
--- /dev/null
+++ b/doc/user/project/pipelines/img/job_artifacts_browser_button.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/job_artifacts_builds_page.png b/doc/user/project/pipelines/img/job_artifacts_builds_page.png
new file mode 100644
index 00000000000..13e039ba934
--- /dev/null
+++ b/doc/user/project/pipelines/img/job_artifacts_builds_page.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/job_artifacts_pipelines_page.png b/doc/user/project/pipelines/img/job_artifacts_pipelines_page.png
new file mode 100644
index 00000000000..3ccce4f9bb4
--- /dev/null
+++ b/doc/user/project/pipelines/img/job_artifacts_pipelines_page.png
Binary files differ
diff --git a/doc/user/project/builds/img/build_latest_artifacts_browser.png b/doc/user/project/pipelines/img/job_latest_artifacts_browser.png
index c6d8856078b..c6d8856078b 100644
--- a/doc/user/project/builds/img/build_latest_artifacts_browser.png
+++ b/doc/user/project/pipelines/img/job_latest_artifacts_browser.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
index 2a99201e014..13ed69be810 100644
--- a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
+++ b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
index c166bb8bec8..fbcd612f3f2 100644
--- a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
+++ b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
Binary files differ
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
new file mode 100644
index 00000000000..5ce99843301
--- /dev/null
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -0,0 +1,143 @@
+# Introduction to job artifacts
+
+>**Notes:**
+>- Since GitLab 8.2 and GitLab Runner 0.7.0, job artifacts that are created by
+ GitLab Runner are uploaded to GitLab and are downloadable as a single archive
+ (`tar.gz`) using the GitLab UI.
+>- Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+ changed to `ZIP`, and it is now possible to browse its contents, with the added
+ ability of downloading the files separately.
+>- Starting with GitLab 8.17, builds are renamed to jobs.
+>- The artifacts browser will be available only for new artifacts that are sent
+ to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
+ browse old artifacts already uploaded to GitLab.
+>- This is the user documentation. For the administration guide see
+ [administration/job_artifacts.md](../../../administration/job_artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a job
+after it completes successfully. This feature is enabled by default in all
+GitLab installations.
+
+## Defining artifacts in `.gitlab-ci.yml`
+
+A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
+the following:
+
+```yaml
+pdf:
+ script: xelatex mycv.tex
+ artifacts:
+ paths:
+ - mycv.pdf
+```
+
+A job named `pdf` calls the `xelatex` command in order to build a pdf file from
+the latex source file `mycv.tex`. We then define the `artifacts` paths which in
+turn are defined with the `paths` keyword. All paths to files and directories
+are relative to the repository that was cloned during the build.
+
+For more examples on artifacts, follow the artifacts reference in
+[`.gitlab-ci.yml` documentation](../../../ci/yaml/README.md#artifacts).
+
+## Browsing job artifacts
+
+After a job finishes, if you visit the job's specific page, you can see
+that there are two buttons. One is for downloading the artifacts archive and
+the other for browsing its contents.
+
+![Job artifacts browser button](img/job_artifacts_browser_button.png)
+
+---
+
+The archive browser shows the name and the actual file size of each file in the
+archive. If your artifacts contained directories, then you are also able to
+browse inside them.
+
+Below you can see how browsing looks like. In this case we have browsed inside
+the archive and at this point there is one directory and one HTML file.
+
+![Job artifacts browser](img/job_artifacts_browser.png)
+
+---
+
+## Downloading job artifacts
+
+If you need to download the whole archive, there are buttons in various places
+inside GitLab that make that possible.
+
+1. While on the pipelines page, you can see the download icon for each job's
+ artifacts archive in the right corner:
+
+ ![Job artifacts in Pipelines page](img/job_artifacts_pipelines_page.png)
+
+1. While on the **Jobs** page, you can see the download icon for each job's
+ artifacts archive in the right corner:
+
+ ![Job artifacts in Builds page](img/job_artifacts_builds_page.png)
+
+1. While inside a specific job, you are presented with a download button
+ along with the one that browses the archive:
+
+ ![Job artifacts browser button](img/job_artifacts_browser_button.png)
+
+1. And finally, when browsing an archive you can see the download button at
+ the top right corner:
+
+ ![Job artifacts browser](img/job_artifacts_browser.png)
+
+## Downloading the latest job artifacts
+
+It is possible to download the latest artifacts of a job via a well known URL
+so you can use it for scripting purposes.
+
+The structure of the URL to download the whole artifacts archive is the following:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
+```
+
+To download a single file from the artifacts use the following URL:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name>
+```
+
+For example, to download the latest artifacts of the job named `coverage` of
+the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
+namespace, the URL would be:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage
+```
+
+To download the file `coverage/index.html` from the same
+artifacts use the following URL:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage
+```
+
+There is also a URL to browse the latest job artifacts:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name>
+```
+
+For example:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage
+```
+
+The latest builds are also exposed in the UI in various places. Specifically,
+look for the download button in:
+
+- the main project's page
+- the branches page
+- the tags page
+
+If the latest job has failed to upload the artifacts, you can see that
+information in the UI.
+
+![Latest artifacts button](img/job_latest_artifacts_browser.png)
+
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 6cbcf3c400f..c398ac2eb25 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -35,7 +35,7 @@ if the job surpasses the threshold, it is marked as failed.
## Test coverage parsing
If you use test coverage in your code, GitLab can capture its output in the
-build log using a regular expression. In the pipelines settings, search for the
+job log using a regular expression. In the pipelines settings, search for the
"Test coverage parsing" section.
![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png)
@@ -44,7 +44,7 @@ Leave blank if you want to disable it or enter a ruby regular expression. You
can use http://rubular.com to test your regex.
If the pipeline succeeds, the coverage is shown in the merge request widget and
-in the builds table.
+in the jobs table.
![MR widget coverage](img/pipelines_test_coverage_mr_widget.png)
@@ -62,9 +62,9 @@ pipelines** checkbox and save the changes.
## Badges
-In the pipelines settings page you can find build status and test coverage
+In the pipelines settings page you can find pipeline status and test coverage
badges for your project. The latest successful pipeline will be used to read
-the build status and test coverage values.
+the pipeline status and test coverage values.
Visit the pipelines settings page in your project to see the exact link to
your badges, as well as ways to embed the badge image in your HTML or Markdown
@@ -72,9 +72,9 @@ pages.
![Pipelines badges](img/pipelines_settings_badges.png)
-### Build status badge
+### Pipeline status badge
-Depending on the status of your build, a badge can have the following values:
+Depending on the status of your job, a badge can have the following values:
- running
- success
@@ -82,7 +82,7 @@ Depending on the status of your build, a badge can have the following values:
- skipped
- unknown
-You can access a build status badge image using the following link:
+You can access a pipeline status badge image using the following link:
```
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg
@@ -91,7 +91,7 @@ https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg
### Test coverage report badge
GitLab makes it possible to define the regular expression for [coverage report],
-that each build log will be matched against. This means that each build in the
+that each job log will be matched against. This means that each job in the
pipeline can have the test coverage percentage value defined.
The test coverage badge can be accessed using following link:
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 675e89e4247..c415d566a7c 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -170,6 +170,5 @@ you commit the changes you will be taken to a new merge request form.
![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-![New file button](basicsimages/file_button.png)
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
+[issue closing pattern]: ../issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index cb1c1a84f8c..be042ddf623 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -14,6 +14,11 @@
> raketask.
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
> every 24 hours by a specific worker.
+> - Group members will get exported as project members, as long as the user has
+> master or admin access to the group where the exported project lives. An admin
+> in the import side is required to map the users, based on email or username.
+> Otherwise, a supplementary comment is left to mention the original author and
+> the MRs, notes or issues will be owned by the importer.
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
@@ -22,7 +27,7 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
-| 8.16.2 to current | 0.1.6 |
+| 8.17.0 to current | 0.1.6 |
| 8.13.0 | 0.1.5 |
| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index a6546cffce2..45176fde9db 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -14,7 +14,7 @@ do.
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
-| `/merge` | Merge (when build succeeds) |
+| `/merge` | Merge (when pipeline succeeds) |
| `/title <New title>` | Change title |
| `/assign @username` | Assign |
| `/unassign` | Remove assignee |
@@ -32,5 +32,7 @@ do.
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
-| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
+| <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/user/snippets.md b/doc/user/snippets.md
new file mode 100644
index 00000000000..417360e08ac
--- /dev/null
+++ b/doc/user/snippets.md
@@ -0,0 +1,19 @@
+# Snippets
+
+Snippets are little bits of code or text.
+
+There are 2 types of snippets - project snippets and personal snippets.
+
+## Project snippets
+
+Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information.
+
+## Personal snippets
+
+Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information).
+
+## Downloading snippets
+
+You can download the raw content of a snippet.
+
+By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 1659dd1f6cb..0ebe5eea173 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,1025 +1 @@
-# Webhooks
-
->**Note:**
-Starting from GitLab 8.5:
-- the `repository` key is deprecated in favor of the `project` key
-- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
-- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
-
-Project webhooks allow you to trigger a URL if for example new code is pushed or
-a new issue is created. You can configure webhooks to listen for specific events
-like pushes, issues or merge requests. GitLab will send a POST request with data
-to the webhook URL.
-
-Webhooks can be used to update an external issue tracker, trigger CI builds,
-update a backup mirror, or even deploy to your production server.
-
-Navigate to the webhooks page by choosing **Webhooks** from your project's
-settings which can be found under the wheel icon in the upper right corner.
-
-## Webhook endpoint tips
-
-If you are writing your own endpoint (web server) that will receive
-GitLab webhooks keep in mind the following things:
-
-- Your endpoint should send its HTTP response as fast as possible. If
- you wait too long, GitLab may decide the hook failed and retry it.
-- Your endpoint should ALWAYS return a valid HTTP response. If you do
- not do this then GitLab will think the hook failed and retry it.
- Most HTTP libraries take care of this for you automatically but if
- you are writing a low-level hook this is important to remember.
-- GitLab ignores the HTTP status code returned by your endpoint.
-
-## Secret token
-
-If you specify a secret token, it will be sent with the hook request in the
-`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
-that the request is legitimate.
-
-## SSL verification
-
-By default, the SSL certificate of the webhook endpoint is verified based on
-an internal list of Certificate Authorities, which means the certificate cannot
-be self-signed.
-
-You can turn this off in the webhook settings in your GitLab projects.
-
-![SSL Verification](ssl.png)
-
-## Events
-
-Below are described the supported events.
-
-### Push events
-
-Triggered when you push to the repository except when pushing tags.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "push",
- "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
- "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "ref": "refs/heads/master",
- "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "user_id": 4,
- "user_name": "John Smith",
- "user_email": "john@example.com",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 15,
- "project":{
- "name":"Diaspora",
- "description":"",
- "web_url":"http://example.com/mike/diaspora",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "namespace":"Mike",
- "visibility_level":0,
- "path_with_namespace":"mike/diaspora",
- "default_branch":"master",
- "homepage":"http://example.com/mike/diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "ssh_url":"git@example.com:mike/diaspora.git",
- "http_url":"http://example.com/mike/diaspora.git"
- },
- "repository":{
- "name": "Diaspora",
- "url": "git@example.com:mike/diaspora.git",
- "description": "",
- "homepage": "http://example.com/mike/diaspora",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "visibility_level":0
- },
- "commits": [
- {
- "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "message": "Update Catalan translation to e38cb41.",
- "timestamp": "2011-12-12T14:27:31+02:00",
- "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "author": {
- "name": "Jordi Mallach",
- "email": "jordi@softcatala.org"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- },
- {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- }
- ],
- "total_commits_count": 4
-}
-```
-
-### Tag events
-
-Triggered when you create (or delete) tags to the repository.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Tag Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "tag_push",
- "before": "0000000000000000000000000000000000000000",
- "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "ref": "refs/tags/v1.0.0",
- "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "user_id": 1,
- "user_name": "John Smith",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 1,
- "project":{
- "name":"Example",
- "description":"",
- "web_url":"http://example.com/jsmith/example",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "git_http_url":"http://example.com/jsmith/example.git",
- "namespace":"Jsmith",
- "visibility_level":0,
- "path_with_namespace":"jsmith/example",
- "default_branch":"master",
- "homepage":"http://example.com/jsmith/example",
- "url":"git@example.com:jsmith/example.git",
- "ssh_url":"git@example.com:jsmith/example.git",
- "http_url":"http://example.com/jsmith/example.git"
- },
- "repository":{
- "name": "Example",
- "url": "ssh://git@example.com/jsmith/example.git",
- "description": "",
- "homepage": "http://example.com/jsmith/example",
- "git_http_url":"http://example.com/jsmith/example.git",
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "visibility_level":0
- },
- "commits": [],
- "total_commits_count": 0
-}
-```
-
-### Issues events
-
-Triggered when a new issue is created or an existing issue was updated/closed/reopened.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Issue Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "issue",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlabhq/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlabhq/gitlab-test"
- },
- "object_attributes": {
- "id": 301,
- "title": "New API: create/update/delete file",
- "assignee_id": 51,
- "author_id": 51,
- "project_id": 14,
- "created_at": "2013-12-03T17:15:43Z",
- "updated_at": "2013-12-03T17:15:43Z",
- "position": 0,
- "branch_name": null,
- "description": "Create new API for manipulations with repository",
- "milestone_id": null,
- "state": "opened",
- "iid": 23,
- "url": "http://example.com/diaspora/issues/23",
- "action": "open"
- },
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
-}
-```
-### Comment events
-
-Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
-The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
-payload will also include information about the target of the comment. For example,
-a comment on a issue will include the specific issue information under the `issue` key.
-Valid target types:
-
-1. `commit`
-2. `merge_request`
-3. `issue`
-4. `snippet`
-
-#### Comment on commit
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1243,
- "note": "This is a commit comment. How does this work?",
- "noteable_type": "Commit",
- "author_id": 1,
- "created_at": "2015-05-17 18:08:09 UTC",
- "updated_at": "2015-05-17 18:08:09 UTC",
- "project_id": 5,
- "attachment":null,
- "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
- "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "noteable_id": null,
- "system": false,
- "st_diff": {
- "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
- "new_path": "six",
- "old_path": "six",
- "a_mode": "0",
- "b_mode": "160000",
- "new_file": true,
- "renamed_file": false,
- "deleted_file": false
- },
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
- },
- "commit": {
- "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "timestamp": "2014-02-27T10:06:20+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
-}
-```
-
-#### Comment on merge request
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://localhost/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1244,
- "note": "This MR needs work.",
- "noteable_type": "MergeRequest",
- "author_id": 1,
- "created_at": "2015-05-17 18:21:36 UTC",
- "updated_at": "2015-05-17 18:21:36 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 7,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
- },
- "merge_request": {
- "id": 7,
- "target_branch": "markdown",
- "source_branch": "master",
- "source_project_id": 5,
- "author_id": 8,
- "assignee_id": 28,
- "title": "Tempora et eos debitis quae laborum et.",
- "created_at": "2015-03-01 20:12:53 UTC",
- "updated_at": "2015-03-21 18:27:27 UTC",
- "milestone_id": 11,
- "state": "opened",
- "merge_status": "cannot_be_merged",
- "target_project_id": 5,
- "iid": 1,
- "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
- "position": 0,
- "locked_at": null,
- "source":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "target": {
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "last_commit": {
- "id": "562e173be03b8ff2efb05345d12df18815438a4b",
- "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
- "timestamp": "2015-04-08T21: 00:25-07:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
- "author": {
- "name": "John Smith",
- "email": "john@example.com"
- }
- },
- "work_in_progress": false,
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-#### Comment on issue
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "description":"",
- "homepage":"http://example.com/mike/diaspora"
- },
- "object_attributes": {
- "id": 1241,
- "note": "Hello world",
- "noteable_type": "Issue",
- "author_id": 1,
- "created_at": "2015-05-17 17:06:40 UTC",
- "updated_at": "2015-05-17 17:06:40 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 92,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
- },
- "issue": {
- "id": 92,
- "title": "test",
- "assignee_id": null,
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-12 14:53:17 UTC",
- "updated_at": "2015-04-26 08:28:42 UTC",
- "position": 0,
- "branch_name": null,
- "description": "test",
- "milestone_id": null,
- "state": "closed",
- "iid": 17
- }
-}
-```
-
-#### Comment on code snippet
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"Gitlab Test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "description":"Aut reprehenderit ut est.",
- "homepage":"http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1245,
- "note": "Is this snippet doing what it's supposed to be doing?",
- "noteable_type": "Snippet",
- "author_id": 1,
- "created_at": "2015-05-17 18:35:50 UTC",
- "updated_at": "2015-05-17 18:35:50 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 53,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
- },
- "snippet": {
- "id": 53,
- "title": "test",
- "content": "puts 'Hello world'",
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-09 02:40:38 UTC",
- "updated_at": "2015-04-09 02:40:38 UTC",
- "file_name": "test.rb",
- "expires_at": null,
- "type": "ProjectSnippet",
- "visibility_level": 0
- }
-}
-```
-
-### Merge request events
-
-Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Merge Request Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "merge_request",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "object_attributes": {
- "id": 99,
- "target_branch": "master",
- "source_branch": "ms-viewport",
- "source_project_id": 14,
- "author_id": 51,
- "assignee_id": 6,
- "title": "MS-Viewport",
- "created_at": "2013-12-03T17:23:34Z",
- "updated_at": "2013-12-03T17:23:34Z",
- "st_commits": null,
- "st_diffs": null,
- "milestone_id": null,
- "state": "opened",
- "merge_status": "unchecked",
- "target_project_id": 14,
- "iid": 1,
- "description": "",
- "source":{
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "target": {
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "last_commit": {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- }
- },
- "work_in_progress": false,
- "url": "http://example.com/diaspora/merge_requests/1",
- "action": "open",
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-### Wiki Page events
-
-Triggered when a wiki page is created or edited.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Wiki Page Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "wiki_page",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
- },
- "project": {
- "name": "awesome-project",
- "description": "This is awesome",
- "web_url": "http://example.com/root/awesome-project",
- "avatar_url": null,
- "git_ssh_url": "git@example.com:root/awesome-project.git",
- "git_http_url": "http://example.com/root/awesome-project.git",
- "namespace": "root",
- "visibility_level": 0,
- "path_with_namespace": "root/awesome-project",
- "default_branch": "master",
- "homepage": "http://example.com/root/awesome-project",
- "url": "git@example.com:root/awesome-project.git",
- "ssh_url": "git@example.com:root/awesome-project.git",
- "http_url": "http://example.com/root/awesome-project.git"
- },
- "wiki": {
- "web_url": "http://example.com/root/awesome-project/wikis/home",
- "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
- "git_http_url": "http://example.com/root/awesome-project.wiki.git",
- "path_with_namespace": "root/awesome-project.wiki",
- "default_branch": "master"
- },
- "object_attributes": {
- "title": "Awesome",
- "content": "awesome content goes here",
- "format": "markdown",
- "message": "adding an awesome page to the wiki",
- "slug": "awesome",
- "url": "http://example.com/root/awesome-project/wikis/awesome",
- "action": "create"
- }
-}
-```
-
-### Pipeline events
-
-Triggered on status change of Pipeline.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Pipeline Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "pipeline",
- "object_attributes":{
- "id": 31,
- "ref": "master",
- "tag": false,
- "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "status": "success",
- "stages":[
- "build",
- "test",
- "deploy"
- ],
- "created_at": "2016-08-12 15:23:28 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "duration": 63
- },
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "project":{
- "name": "Gitlab Test",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "avatar_url": null,
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "namespace": "Gitlab Org",
- "visibility_level": 20,
- "path_with_namespace": "gitlab-org/gitlab-test",
- "default_branch": "master"
- },
- "commit":{
- "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "message": "test\n",
- "timestamp": "2016-08-12T17:23:21+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "author":{
- "name": "User",
- "email": "user@gitlab.com"
- }
- },
- "builds":[
- {
- "id": 380,
- "stage": "deploy",
- "name": "production",
- "status": "skipped",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "manual",
- "manual": true,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 377,
- "stage": "test",
- "name": "test-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 378,
- "stage": "test",
- "name": "test-build",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 376,
- "stage": "build",
- "name": "build-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:24:56 UTC",
- "finished_at": "2016-08-12 15:25:26 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 379,
- "stage": "deploy",
- "name": "staging",
- "status": "created",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- }
- ]
-}
-```
-
-### Build events
-
-Triggered on status change of a Build.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Build Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "build",
- "ref": "gitlab-script-trigger",
- "tag": false,
- "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "build_id": 1977,
- "build_name": "test",
- "build_stage": "test",
- "build_status": "created",
- "build_started_at": null,
- "build_finished_at": null,
- "build_duration": null,
- "build_allow_failure": false,
- "project_id": 380,
- "project_name": "gitlab-org/gitlab-test",
- "user": {
- "id": 3,
- "name": "User",
- "email": "user@gitlab.com"
- },
- "commit": {
- "id": 2366,
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "message": "test\n",
- "author_name": "User",
- "author_email": "user@gitlab.com",
- "status": "created",
- "duration": null,
- "started_at": null,
- "finished_at": null
- },
- "repository": {
- "name": "gitlab_test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "visibility_level": 20
- }
-}
-```
-
-## Example webhook receiver
-
-If you want to see GitLab's webhooks in action for testing purposes you can use
-a simple echo script running in a console session. For the following script to
-work you need to have Ruby installed.
-
-Save the following file as `print_http_body.rb`:
-
-```ruby
-require 'webrick'
-
-server = WEBrick::HTTPServer.new(:Port => ARGV.first)
-server.mount_proc '/' do |req, res|
- puts req.body
-end
-
-trap 'INT' do
- server.shutdown
-end
-server.start
-```
-
-Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
-8000`. Then add your server as a webhook receiver in GitLab as
-`http://my.host:8000/`.
-
-When you press 'Test Hook' in GitLab, you should see something like this in the
-console:
-
-```
-{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
-example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
-- -> /
-```
+This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md).
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 0b6f00c6aa4..9e7ee47387c 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -27,7 +27,7 @@
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
-- [Merge Requests](../user/project/merge_requests.md)
+- [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
@@ -39,3 +39,4 @@
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
+- [Snippets](../user/snippets.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index c228ea72f22..d12c0c6d0c4 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions.
![Master branch and production branch with arrow that indicate deployments](production_branch.png)
GitHub flow does assume you are able to deploy to production every time you merge a feature branch.
-This is possible for SaaS applications but are many cases where this is not possible.
+This is possible for SaaS applications but there are many cases where this is not possible.
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation.
Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times.
In these cases you can make a production branch that reflects the deployed code.
@@ -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/groups.md b/doc/workflow/groups.md
index a693cc3d0fd..6237a5d5e18 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -23,7 +23,7 @@ You can use the 'New project' button to add a project to the new group.
## Transferring an existing project into a group
-You can transfer an existing project into a group you own from the project settings page.
+You can transfer an existing project into a group you own from the project settings page. The option to transfer a project is only available if you are the Owner of the project.
First scroll down to the 'Dangerous settings' and click 'Show them to me'.
Now you can pick any of the groups you manage as the new namespace for the group.
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 97380bce172..f3c636ed1d5 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -28,7 +28,7 @@ to enable this if not already.
When issues/pull requests are being imported, the Bitbucket importer tries to find
the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and [**associated their Bitbucket account**][social sign-in]. If the user is not
+and **associated their Bitbucket account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original Bitbucket author is kept.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 86a016fc6d6..aece4ab34ba 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -28,7 +28,7 @@ still be able to import their GitHub repositories with a
When issues/pull requests are being imported, the GitHub importer tries to find
the GitHub author/assignee in GitLab's database using the GitHub ID. For this
to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and [**associated their GitHub account**][social sign-in]. If the user is not
+and **associated their GitHub account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original GitHub author is kept.
@@ -60,8 +60,7 @@ If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
you can use it instead of the personal access token.
1. First you may want to connect your GitHub account to GitLab in order for
- the username mapping to be correct. Follow the [social sign-in] documentation
- on how to do so.
+ the username mapping to be correct.
1. Once you connect GitHub, click the **List your GitHub repositories** button
and you will be redirected to GitHub for permission to access your projects.
1. After accepting, you'll be automatically redirected to the importer.
@@ -115,4 +114,3 @@ if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
-[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 5f6a718135d..3a6773909d6 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -43,8 +43,8 @@ In `config/gitlab.yml`:
## Storage statistics
You can see the total storage used for LFS objects on groups and projects
-in the administration area, as well as through the [groups](../api/groups.md)
-and [projects APIs](../api/projects.md).
+in the administration area, as well as through the [groups](../../api/groups.md)
+and [projects APIs](../../api/projects.md).
## Known limitations
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/shortcuts.md b/doc/workflow/shortcuts.md
index 36516883ef6..7aa9b46081a 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -45,9 +45,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to builds |
+| <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 |
@@ -73,4 +73,4 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>m</kbd> | Change milestone |
| <kbd>r</kbd> | Reply (quoting selected text) |
| <kbd>e</kbd> | Edit issue/merge request |
-| <kbd>l</kbd> | Change label | \ No newline at end of file
+| <kbd>l</kbd> | Change label |
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 1a8fc39bb33..4b0fba842e9 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -16,7 +16,8 @@ in a simple dashboard.
You can quickly access the Todos dashboard using the bell icon next to the
search bar in the upper right corner. The number in blue is the number of Todos
-you still have open.
+you still have open if the count is < 100, else it's 99+. The exact number
+will still be shown in the body of the _To do_ tab.
![Todos icon](img/todos_icon.png)
@@ -27,11 +28,34 @@ A Todo appears in your Todos dashboard when:
- an issue or merge request is assigned to you,
- you are `@mentioned` in an issue or merge request, be it the description of
the issue/merge request or in a comment,
-- build in the CI pipeline running for your merge request failed, but this
- build is not allowed to fail.
+- a job in the CI pipeline running for your merge request failed, but this
+ job is not allowed to fail.
>**Note:** Commenting on a commit will _not_ trigger a Todo.
+### Directly addressed Todos
+
+> [Introduced][ce-7926] in GitLab 9.0.
+
+If you are mentioned at the start of a line, the todo you receive will be listed
+as 'directly addressed'. For instance, in this comment:
+
+```markdown
+@alice What do you think? cc: @bob
+
+- @carol can you please have a look?
+
+>>>
+@dan what do you think?
+>>>
+
+@erin @frank thank you!
+```
+
+The people receiving directly addressed todos are `@alice`, `@erin`, and
+`@frank`. Directly addressed todos only differ from mention todos in their type,
+for filtering; otherwise, they appear as normal.
+
### Manually creating a Todo
You can also add an issue or merge request to your Todos dashboard by clicking
@@ -85,8 +109,9 @@ There are four kinds of filters you can use on your Todos dashboard.
| Project | Filter by project |
| Author | Filter by the author that triggered the Todo |
| Type | Filter by issue or merge request |
-| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)|
+| Action | Filter by the action that triggered the Todo |
You can also filter by more than one of these at the same time.
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
+[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
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/dashboard/issues.feature b/features/dashboard/issues.feature
deleted file mode 100644
index 99dad88a402..00000000000
--- a/features/dashboard/issues.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-@dashboard
-Feature: Dashboard Issues
- Background:
- Given I sign in as a user
- And I have authored issues
- And I have assigned issues
- And I have other issues
- And I visit dashboard issues page
-
- Scenario: I should see assigned issues
- Then I should see issues assigned to me
-
- @javascript
- Scenario: I should see authored issues
- When I click "Authored by me" link
- Then I should see issues authored by me
-
- @javascript
- Scenario: I should see all issues
- When I click "All" link
- Then I should see all issues
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index d033e6b167b..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,29 +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 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
@@ -73,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
@@ -86,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/labels.feature b/features/project/labels.feature
deleted file mode 100644
index 955bc3d8b1b..00000000000
--- a/features/project/labels.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-@labels
-Feature: Labels
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has labels: "bug", "feature", "enhancement"
- When I visit project "Shop" labels page
-
- @javascript
- Scenario: I can subscribe to a label
- Then I should see that I am not subscribed to the "bug" label
- When I click button "Subscribe" for the "bug" label
- Then I should see that I am subscribed to the "bug" label
- When I click button "Unsubscribe" for the "bug" label
- Then I should see that I am not subscribed to the "bug" label
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 5aa592e9067..bcde497553b 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -294,13 +294,6 @@ Feature: Project Merge Requests
Then I should see the Markdown write tab
@javascript
- Scenario: I search merge request
- Given I click link "All"
- When I fill in merge request search with "Fe"
- Then I should see "Feature NS-03" in merge requests
- And I should not see "Bug NS-04" in merge requests
-
- @javascript
Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04"
Then I should see that I am subscribed
diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature
index d767b088883..ec6666f227f 100644
--- a/features/project/merge_requests/revert.feature
+++ b/features/project/merge_requests/revert.feature
@@ -5,6 +5,7 @@ Feature: Revert Merge Requests
And I am signed in as a developer of the project
And I am on the Merge Request detail page
And I click on Accept Merge Request
+ And I am on the Merge Request detail page
@javascript
Scenario: I revert a merge request
diff --git a/features/project/pages.feature b/features/project/pages.feature
new file mode 100644
index 00000000000..87d88348d09
--- /dev/null
+++ b/features/project/pages.feature
@@ -0,0 +1,82 @@
+Feature: Project Pages
+ Background:
+ Given I sign in as a user
+ And I own a project
+
+ Scenario: Pages are disabled
+ Given pages are disabled
+ When I visit the Project Pages
+ Then I should see that GitLab Pages are disabled
+
+ Scenario: I can see the pages usage if not deployed
+ Given pages are enabled
+ When I visit the Project Pages
+ Then I should see the usage of GitLab Pages
+
+ Scenario: I can access the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ Then I should be able to access the Pages
+
+ Scenario: I should message that domains support is disabled
+ Given pages are enabled
+ And pages are deployed
+ And support for external domains is disabled
+ When I visit the Project Pages
+ Then I should see that support for domains is disabled
+
+ Scenario: I should see a new domain button
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit the Project Pages
+ And I should be able to add a New Domain
+
+ Scenario: I should be able to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be able to add a new domain for project in group namespace
+ Given I own a project in some group namespace
+ And pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be denied to add the same domain twice
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see error message that domain already exists
+
+ Scenario: I should message that certificates support is disabled when trying to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ Then I should see that support for certificates is disabled
+
+ Scenario: I should be able to add a new domain with certificate
+ Given pages are enabled
+ And pages are exposed on external HTTPS address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I fill the certificate and key
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I can remove the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ And I click Remove Pages
+ Then The Pages should get removed
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/snippets/user.feature b/features/snippets/user.feature
deleted file mode 100644
index 5b5dadb7b39..00000000000
--- a/features/snippets/user.feature
+++ /dev/null
@@ -1,34 +0,0 @@
-@snippets
-Feature: Snippets User
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have internal "Personal snippet internal" snippet
-
- Scenario: I should see all my snippets
- Given I visit my snippets page
- Then I should see "Personal snippet one" in snippets
- And I should see "Personal snippet private" in snippets
- And I should see "Personal snippet internal" in snippets
-
- Scenario: I can see only my private snippets
- Given I visit my snippets page
- And I click "Private" filter
- Then I should not see "Personal snippet one" in snippets
- And I should not see "Personal snippet internal" in snippets
- And I should see "Personal snippet private" in snippets
-
- Scenario: I can see only my public snippets
- Given I visit my snippets page
- And I click "Public" filter
- Then I should see "Personal snippet one" in snippets
- And I should not see "Personal snippet private" in snippets
- And I should not see "Personal snippet internal" in snippets
-
- Scenario: I can see only my internal snippets
- Given I visit my snippets page
- And I click "Internal" filter
- Then I should see "Personal snippet internal" in snippets
- And I should not see "Personal snippet private" in snippets
- And I should not see "Personal snippet one" in snippets
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
deleted file mode 100644
index 4e15d79ae74..00000000000
--- a/features/steps/dashboard/issues.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include Select2Helper
-
- step 'I should see issues assigned to me' do
- should_see(assigned_issue)
- should_not_see(authored_issue)
- should_not_see(other_issue)
- end
-
- step 'I should see issues authored by me' do
- should_see(authored_issue)
- should_see(authored_issue_on_public_project)
- should_not_see(assigned_issue)
- should_not_see(other_issue)
- end
-
- step 'I should see all issues' do
- should_see(authored_issue)
- should_see(assigned_issue)
- should_see(other_issue)
- end
-
- step 'I have authored issues' do
- authored_issue
- authored_issue_on_public_project
- end
-
- step 'I have assigned issues' do
- assigned_issue
- end
-
- step 'I have other issues' do
- other_issue
- end
-
- step 'I click "Authored by me" link' do
- find("#assignee_id").set("")
- find(".js-author-search", match: :first).click
- find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
- end
-
- step 'I click "All" link' do
- find(".js-author-search").click
- expect(page).to have_selector(".dropdown-menu-author li a")
- find(".dropdown-menu-author li a", match: :first).click
- expect(page).not_to have_selector(".dropdown-menu-author li a")
-
- find(".js-assignee-search").click
- expect(page).to have_selector(".dropdown-menu-assignee li a")
- find(".dropdown-menu-assignee li a", match: :first).click
- expect(page).not_to have_selector(".dropdown-menu-assignee li a")
- end
-
- def should_see(issue)
- expect(page).to have_content(issue.title[0..10])
- end
-
- def should_not_see(issue)
- expect(page).not_to have_content(issue.title[0..10])
- end
-
- def assigned_issue
- @assigned_issue ||= create :issue, assignee: current_user, project: project
- end
-
- def authored_issue
- @authored_issue ||= create :issue, author: current_user, project: project
- end
-
- def other_issue
- @other_issue ||= create :issue, project: project
- end
-
- def authored_issue_on_public_project
- @authored_issue_on_public_project ||= create :issue, author: current_user, project: public_project
- end
-
- def project
- @project ||= begin
- project = create(:empty_project)
- project.team << [current_user, :master]
- project
- end
- end
-
- def public_project
- @public_project ||= create(:empty_project, :public)
- end
-end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 2bbc43b491f..eb906a55a83 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -47,7 +47,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.todos-pending-count') { expect(page).to have_content '3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible)
end
step 'I mark all todos as done' do
@@ -71,7 +71,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done 1'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible)
end
step 'I should see all todos marked as done' do
@@ -81,10 +81,10 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done 4'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false)
- should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false)
- should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false)
- should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible)
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible)
end
step 'I filter by "Enterprise"' do
@@ -140,15 +140,20 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.should have_css('.identifier', text: 'Merge Request !1')
end
- def should_see_todo(position, title, body, pending = true)
+ def should_see_todo(position, title, body, state: :pending)
page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title
expect(page).to have_content body
- if pending
+ if state == :pending
expect(page).to have_link 'Done'
- else
+ elsif state == :done_reversible
+ expect(page).to have_link 'Undo'
+ elsif state == :done_irreversible
+ expect(page).not_to have_link 'Undo'
expect(page).not_to have_link 'Done'
+ else
+ raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible'
end
end
end
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 9f701840f1d..4befd49ac81 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -22,29 +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 'the active sub nav should be Members' do
- ensure_active_sub_nav('Members')
+ step 'I click the "Pages" tab' do
+ page.within '.sub-nav' do
+ click_link('Pages')
+ end
+ end
+
+ 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 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
@@ -63,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/builds/summary.rb b/features/steps/project/builds/summary.rb
index 374eb0b0e07..19ff92f6dc6 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
step 'recent build summary contains information saying that build has been erased' do
page.within('.erased') do
- expect(page).to have_content 'Build has been erased'
+ expect(page).to have_content 'Job has been erased'
end
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/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 7490d2bc6e7..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.* %/
@@ -34,9 +38,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Builds for last week'
- expect(page).to have_content 'Builds for last month'
- expect(page).to have_content 'Builds for last year'
+ expect(page).to have_content 'Jobs for last week'
+ expect(page).to have_content 'Jobs for last month'
+ expect(page).to have_content 'Jobs for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
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/labels.rb b/features/steps/project/labels.rb
deleted file mode 100644
index dbeb07c78db..00000000000
--- a/features/steps/project/labels.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class Spinach::Features::Labels < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedPaths
-
- step 'And I visit project "Shop" labels page' do
- visit namespace_project_labels_path(project.namespace, project)
- end
-
- step 'I should see that I am subscribed to the "bug" label' do
- expect(subscribe_button).to have_content 'Unsubscribe'
- end
-
- step 'I should see that I am not subscribed to the "bug" label' do
- expect(subscribe_button).to have_content 'Subscribe'
- end
-
- step 'I click button "Unsubscribe" for the "bug" label' do
- subscribe_button.click
- end
-
- step 'I click button "Subscribe" for the "bug" label' do
- subscribe_button.click
- end
-
- private
-
- def subscribe_button
- first('.js-subscribe-button', visible: true)
- end
-end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index d2fa8cd39af..9f0057cace7 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe"
+ page.within '.merge-requests-holder' do
+ find('.merge-request')
+ end
end
step 'I click the "Target branch" dropdown' do
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
new file mode 100644
index 00000000000..c80c6273807
--- /dev/null
+++ b/features/steps/project/pages.rb
@@ -0,0 +1,139 @@
+class Spinach::Features::ProjectPages < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+
+ step 'pages are enabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:host).and_return('example.com')
+ allow(Gitlab.config.pages).to receive(:port).and_return(80)
+ allow(Gitlab.config.pages).to receive(:https).and_return(false)
+ end
+
+ step 'pages are disabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ step 'I visit the Project Pages' do
+ visit namespace_project_pages_path(@project.namespace, @project)
+ end
+
+ step 'I should see that GitLab Pages are disabled' do
+ expect(page).to have_content('GitLab Pages are disabled')
+ end
+
+ step 'I should see the usage of GitLab Pages' do
+ expect(page).to have_content('Configure pages')
+ end
+
+ step 'pages are deployed' do
+ pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha)
+ build = build(:ci_build,
+ project: @project,
+ pipeline: pipeline,
+ ref: 'HEAD',
+ artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'),
+ artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
+ )
+ result = ::Projects::UpdatePagesService.new(@project, build).execute
+ expect(result[:status]).to eq(:success)
+ end
+
+ step 'I should be able to access the Pages' do
+ expect(page).to have_content('Access pages')
+ end
+
+ step 'I should see that support for domains is disabled' do
+ expect(page).to have_content('Support for domains and certificates is disabled')
+ end
+
+ step 'support for external domains is disabled' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ 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_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')
+ end
+
+ step 'I should be able to add a New Domain' do
+ expect(page).to have_content('New Domain')
+ end
+
+ step 'I visit add a new Pages Domain' do
+ visit new_namespace_project_pages_domain_path(@project.namespace, @project)
+ end
+
+ step 'I fill the domain' do
+ fill_in 'Domain', with: 'my.test.domain.com'
+ end
+
+ step 'I click on "Create New Domain"' do
+ click_button 'Create New Domain'
+ end
+
+ step 'I should see a new domain added' do
+ expect(page).to have_content('Domains (1)')
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ step 'pages domain is added' do
+ @project.pages_domains.create!(domain: 'my.test.domain.com')
+ end
+
+ step 'I should see error message that domain already exists' do
+ expect(page).to have_content('Domain has already been taken')
+ end
+
+ step 'I should see that support for certificates is disabled' do
+ expect(page).to have_content('Support for custom certificates is disabled')
+ end
+
+ step 'I fill the certificate and key' do
+ fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+
+ fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ step 'I click Remove Pages' do
+ click_link 'Remove pages'
+ end
+
+ step 'The Pages should get removed' do
+ expect(@project.pages_deployed?).to be_falsey
+ end
+end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce..6845f75f22f 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -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"
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 70e6d4836b2..5bc3a1f5ac4 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build_with_coverage, pipeline: @pipeline)
+ @build = create(:ci_build, :coverage, pipeline: @pipeline)
end
step 'recent build is successful' do
@@ -47,7 +47,7 @@ module SharedBuilds
end
step 'recent build has a build trace' do
- @build.trace = 'build trace'
+ @build.trace = 'job trace'
end
step 'download of build artifacts archive starts' do
@@ -60,7 +60,7 @@ module SharedBuilds
end
step 'I see details of a build' do
- expect(page).to have_content "Build ##{@build.id}"
+ expect(page).to have_content "Job ##{@build.id}"
end
step 'I see build trace' do
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 7a6707a7dfb..345a28f27dc 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -7,6 +7,12 @@ module SharedProject
@project.team << [@user, :master]
end
+ step "I own a project in some group namespace" do
+ @group = create(:group, name: 'some group')
+ @project = create(:project, namespace: @group)
+ @project.team << [@user, :master]
+ end
+
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
@project = create(:project, :repository, namespace: @group, public_builds: false)
@@ -160,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/steps/snippets/user.rb b/features/steps/snippets/user.rb
deleted file mode 100644
index 997c605bce2..00000000000
--- a/features/steps/snippets/user.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
-
- step 'I visit my snippets page' do
- visit dashboard_snippets_path
- end
-
- step 'I should see "Personal snippet one" in snippets' do
- expect(page).to have_content "Personal snippet one"
- end
-
- step 'I should see "Personal snippet private" in snippets' do
- expect(page).to have_content "Personal snippet private"
- end
-
- step 'I should see "Personal snippet internal" in snippets' do
- expect(page).to have_content "Personal snippet internal"
- end
-
- step 'I should not see "Personal snippet one" in snippets' do
- expect(page).not_to have_content "Personal snippet one"
- end
-
- step 'I should not see "Personal snippet private" in snippets' do
- expect(page).not_to have_content "Personal snippet private"
- end
-
- step 'I should not see "Personal snippet internal" in snippets' do
- expect(page).not_to have_content "Personal snippet internal"
- end
-
- step 'I click "Internal" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Internal"
- end
- end
-
- step 'I click "Private" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Private"
- end
- end
-
- step 'I click "Public" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Public"
- end
- end
-
- def snippet
- @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
- end
-end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 47372df152d..a5fcbb65131 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -2,7 +2,7 @@ require 'spinach/capybara'
require 'capybara/poltergeist'
# 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|
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/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
new file mode 100644
index 00000000000..2358fa6bbfd
--- /dev/null
+++ b/lib/additional_email_headers_interceptor.rb
@@ -0,0 +1,8 @@
+class AdditionalEmailHeadersInterceptor
+ def self.delivering_email(message)
+ message.headers(
+ 'Auto-Submitted' => 'auto-generated',
+ 'X-Auto-Response-Suppress' => 'All'
+ )
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cf6b501021..1bf20f76ad6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -1,7 +1,47 @@
module API
class API < Grape::API
include APIGuard
- version 'v3', using: :path
+
+ 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::Templates
+ mount ::API::V3::Todos
+ mount ::API::V3::Triggers
+ mount ::API::V3::Users
+ mount ::API::V3::Variables
+ end
before { allow_access_with_scope :api }
@@ -23,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
@@ -40,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
@@ -50,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
@@ -65,6 +109,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 58a4df54bea..f9e0c2c4e16 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -3,19 +3,24 @@ module API
include PaginationParams
before { authenticate! }
- AWARDABLES = %w[issue merge_request snippet]
+ AWARDABLES = [
+ { type: 'issue', find_by: :iid },
+ { type: 'merge_request', find_by: :iid },
+ { type: 'snippet', find_by: :id }
+ ].freeze
resource :projects do
- AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.pluralize
- awardable_id_string = "#{awardable_type}_id"
+ 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|
@@ -28,8 +33,8 @@ module API
end
get endpoint do
if can_read_awardable?
- awards = paginate(awardable.award_emoji)
- present awards, with: Entities::AwardEmoji
+ awards = awardable.award_emoji
+ present paginate(awards), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
@@ -82,7 +87,6 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
award.destroy
- present award, with: Entities::AwardEmoji
end
end
end
@@ -104,10 +108,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 4ac491edc1b..b6843c1b6af 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,6 +1,7 @@
module API
- # Boards API
class Boards < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -11,9 +12,12 @@ module API
detail 'This feature was introduced in 8.13'
success Entities::Board
end
+ params do
+ use :pagination
+ end
get ':id/boards' do
authorize!(:read_board, user_project)
- present user_project.boards, with: Entities::Board
+ present paginate(user_project.boards), with: Entities::Board
end
params do
@@ -37,12 +41,15 @@ module API
end
desc 'Get the lists of a project board' do
- detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
+ params do
+ use :pagination
+ end
get '/lists' do
authorize!(:read_board, user_project)
- present board_lists, with: Entities::List
+ present paginate(board_lists), with: Entities::List
end
desc 'Get a list of a project board' do
@@ -120,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 be659fa4a6a..73a7e939627 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -1,8 +1,9 @@
require 'mime/types'
module API
- # Projects API
class Branches < Grape::API
+ include PaginationParams
+
before { authenticate! }
before { authorize! :download_code, user_project }
@@ -13,10 +14,13 @@ module API
desc 'Get a project repository branches' do
success Entities::RepoBranch
end
+ params do
+ use :pagination
+ end
get ":id/repository/branches" do
- branches = user_project.repository.branches.sort_by(&:name)
+ branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
- present branches, with: Entities::RepoBranch, project: user_project
+ present paginate(branches), with: Entities::RepoBranch, project: user_project
end
desc 'Get a single branch' do
@@ -84,7 +88,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
protected_branch = user_project.protected_branches.find_by(name: branch.name)
- protected_branch.destroy if protected_branch
+ protected_branch&.destroy
present branch, with: Entities::RepoBranch, project: user_project
end
@@ -93,13 +97,13 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :branch, 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])
+ execute(params[:branch], params[:ref])
if result[:status] == :success
present result[:branch],
@@ -120,11 +124,7 @@ module API
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
- if result[:status] == :success
- {
- branch_name: params[:branch]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
@@ -133,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 af61be343be..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 Build") 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 b6e6820c3f4..9d9f82fdb83 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -40,12 +40,13 @@ module API
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'
optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"'
+ optional :coverage, type: Float, desc: 'The total code coverage'
end
post ':id/statuses/:sha' do
authorize! :create_commit_status, user_project
@@ -71,13 +72,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]
+ 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 e6d707f3c3d..42401abfe0f 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -16,27 +16,36 @@ module API
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 :since, type: String, desc: 'Only commits after or in this date will be returned'
- optional :until, type: String, desc: 'Only commits before or in 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 :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 :path, type: String, desc: 'The file path'
+ use :pagination
end
get ":id/repository/commits" do
- # TODO remove the next line for 9.0, use DateTime type in the params block
- datetime_attributes! :since, :until
-
- 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
- present commits, with: Entities::RepoCommit
+ paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
+
+ present paginate(paginated_commits), with: Entities::RepoCommit
end
desc 'Commit multiple file changes as one commit' do
@@ -44,7 +53,7 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :branch_name, type: String, desc: 'The name of branch'
+ requires :branch, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
optional :author_email, type: String, desc: 'Author email for commit'
@@ -53,15 +62,7 @@ module API
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared_params
- attrs[:source_branch] = attrs[:branch_name]
- attrs[:target_branch] = attrs[:branch_name]
- 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
+ attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -114,7 +115,7 @@ module API
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = Note.where(commit_id: commit.id).order(:created_at)
+ notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
present paginate(notes), with: Entities::CommitNote
end
@@ -138,9 +139,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]
}
@@ -163,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 64da7d6b86f..69e85c27a65 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,13 +1,17 @@
module API
- # Projects API
class DeployKeys < Grape::API
+ include PaginationParams
+
before { authenticate! }
+ desc 'Return all deploy keys'
+ params do
+ use :pagination
+ end
get "deploy_keys" do
authenticated_as_admin!
- keys = DeployKey.all
- present keys, with: Entities::SSHKey
+ present paginate(DeployKey.all), with: Entities::SSHKey
end
params do
@@ -16,108 +20,90 @@ module API
resource :projects do
before { authorize_admin_project }
- # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "projects/:id/deploy_keys/..." instead.
- #
- %w(keys deploy_keys).each do |path|
- desc "Get a specific project's deploy keys" do
- success Entities::SSHKey
- end
- get ":id/#{path}" do
- present user_project.deploy_keys, with: Entities::SSHKey
- end
-
- desc 'Get single deploy key' do
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- get ":id/#{path}/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
- present key, with: Entities::SSHKey
- end
-
- desc 'Add new deploy key to currently authenticated user' do
- success Entities::SSHKey
- end
- params do
- requires :key, type: String, desc: 'The new deploy key'
- requires :title, type: String, desc: 'The name of the deploy key'
- end
- post ":id/#{path}" do
- params[:key].strip!
+ desc "Get a specific project's deploy keys" do
+ success Entities::SSHKey
+ end
+ params do
+ use :pagination
+ end
+ get ":id/deploy_keys" do
+ present paginate(user_project.deploy_keys), with: Entities::SSHKey
+ end
- # Check for an existing key joined to this project
- key = user_project.deploy_keys.find_by(key: params[:key])
- if key
- present key, with: Entities::SSHKey
- break
- end
+ desc 'Get single deploy key' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: Entities::SSHKey
+ end
- # Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: params[:key])
- if key
- user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- break
- end
+ desc 'Add new deploy key to currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/deploy_keys" do
+ params[:key].strip!
- # Create a new deploy key
- key = DeployKey.new(declared_params(include_missing: false))
- if key.valid? && user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- else
- render_validation_error!(key)
- end
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: Entities::SSHKey
+ break
end
- desc 'Enable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ break
end
- post ":id/#{path}/:key_id/enable" do
- key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
- if key
- present key, with: Entities::SSHKey
- else
- not_found!('Deploy Key')
- end
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ else
+ render_validation_error!(key)
end
+ end
- desc 'Disable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id/disable" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- key.destroy
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/deploy_keys/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
- present key.deploy_key, with: Entities::SSHKey
+ if key
+ present key, with: Entities::SSHKey
+ else
+ not_found!('Deploy Key')
end
+ end
- desc 'Delete deploy key for a project' do
- success Key
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- if key
- key.destroy
- else
- not_found!('Deploy Key')
- end
- end
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ not_found!('Deploy Key') unless key
+
+ key.destroy
end
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index c5feb49b22f..2f1ad12c38c 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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index a07b2a9ca0f..0a12ee72d49 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -26,7 +26,7 @@ module API
expose :last_sign_in_at
expose :confirmed_at
expose :email
- expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
@@ -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,19 +132,20 @@ 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
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
+ expose :build_artifacts_size, as: :job_artifacts_size
end
end
end
@@ -154,10 +155,27 @@ module API
expose :shared_projects, using: Entities::Project
end
+ class RepoCommit < Grape::Entity
+ expose :id, :short_id, :title, :created_at
+ expose :parent_ids
+ expose :safe_message, as: :message
+ expose :author_name, :author_email, :authored_date
+ expose :committer_name, :committer_email, :committed_date
+ end
+
+ class RepoCommitStats < Grape::Entity
+ expose :additions, :deletions, :total
+ end
+
+ class RepoCommitDetail < RepoCommit
+ expose :stats, using: Entities::RepoCommitStats
+ expose :status
+ end
+
class RepoBranch < Grape::Entity
expose :name
- expose :commit do |repo_branch, options|
+ expose :commit, using: Entities::RepoCommit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
@@ -192,30 +210,11 @@ module API
end
end
- class RepoCommit < Grape::Entity
- expose :id, :short_id, :title, :author_name, :author_email, :created_at
- expose :committer_name, :committer_email
- expose :safe_message, as: :message
- end
-
- class RepoCommitStats < Grape::Entity
- expose :additions, :deletions, :total
- end
-
- class RepoCommitDetail < RepoCommit
- expose :parent_ids, :committed_date, :authored_date
- expose :stats, using: Entities::RepoCommitStats
- expose :status
- end
-
class ProjectSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
- # TODO (rspeicher): Deprecated; remove in 9.0
- expose(:expires_at) { |snippet| nil }
-
expose :web_url do |snippet, options|
Gitlab::UrlBuilder.build(snippet)
end
@@ -251,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
@@ -269,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
@@ -281,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
@@ -289,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
@@ -305,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
@@ -340,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
@@ -369,7 +371,7 @@ module API
class CommitStatus < Grape::Entity
expose :id, :sha, :ref, :status, :name, :target_url, :description,
- :created_at, :started_at, :finished_at, :allow_failure
+ :created_at, :started_at, :finished_at, :allow_failure, :coverage
expose :author, using: Entities::UserBasic
end
@@ -382,9 +384,7 @@ module API
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options|
- if event.author
- event.author.username
- end
+ event.author&.username
end
end
@@ -400,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|
@@ -418,7 +419,7 @@ module API
end
class Namespace < Grape::Entity
- expose :id, :name, :path, :kind
+ expose :id, :name, :path, :kind, :full_path
end
class MemberAccess < Grape::Entity
@@ -454,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.
@@ -557,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
@@ -575,6 +580,7 @@ module API
expose :koding_url
expose :plantuml_enabled
expose :plantuml_url
+ expose :terminal_max_session_time
end
class Release < Grape::Entity
@@ -594,10 +600,6 @@ module API
end
end
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- end
-
class Runner < Grape::Entity
expose :id
expose :description
@@ -622,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
@@ -630,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
@@ -662,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
@@ -696,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
+ 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 :depends_on_builds, as: :dependencies, using: Dependency
+ end
+ end
end
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 1a7e68f0528..ebe8c3a5b2c 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -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 2e79e22e649..bb8f5c3076d 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,12 +1,11 @@
module API
- # Projects API
class Files < Grape::API
helpers do
def commit_params(attrs)
{
file_path: attrs[:file_path],
- source_branch: attrs[:branch_name],
- target_branch: attrs[:branch_name],
+ start_branch: attrs[:branch],
+ target_branch: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
@@ -15,16 +14,29 @@ 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],
- branch_name: attrs[:branch_name]
+ branch: attrs[:branch]
}
end
params :simple_file_params do
- requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
- requires :branch_name, type: String, desc: 'The name of branch'
+ 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'
optional :author_name, type: String, desc: 'The name of the author'
@@ -41,34 +53,35 @@ module API
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
- desc 'Get a file from repository'
+ 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
@@ -76,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)
@@ -94,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)
@@ -113,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::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
+ 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 7682d286866..b862ff70b31 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,23 +59,13 @@ 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
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
@@ -99,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
@@ -125,7 +118,7 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- DestroyGroupService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).execute
end
desc 'Get a list of projects in this group.' do
@@ -133,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],
@@ -142,6 +135,9 @@ module API
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
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a1d7b323f4f..bd22b82476b 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 = {})
@@ -45,7 +45,7 @@ module API
if id =~ /^\d+$/
Project.find_by(id: id)
else
- Project.find_with_namespace(id)
+ Project.find_by_full_path(id)
end
end
@@ -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!
@@ -116,7 +116,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
@@ -134,7 +134,7 @@ module API
end
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -153,33 +153,21 @@ module API
params_hash = custom_params || params
attrs = {}
keys.each do |key|
- if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
+ if params_hash[key].present? || (params_hash.has_key?(key) && params_hash[key] == false)
attrs[key] = params_hash[key]
end
end
ActionController::Parameters.new(attrs).permit!
end
- # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
- # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
- #
- # Parameters:
- # keys (required) - An array consisting of elements that must be parseable as dates from the params hash
- def datetime_attributes!(*keys)
- keys.each do |key|
- begin
- params[key] = Time.xmlschema(params[key]) if params[key].present?
- rescue ArgumentError
- message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
- render_api_error!(message, 400)
- end
- end
- end
-
def filter_by_iid(items, iid)
items.where(iid: iid)
end
+ def filter_by_search(items, text)
+ items.search(text)
+ end
+
# error helpers
def forbidden!(reason = nil)
@@ -225,12 +213,20 @@ 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)
end
end
+ def render_spam_error!
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
def render_api_error!(message, status)
error!({ 'message' => message }, status, header)
end
@@ -256,6 +252,18 @@ 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
+
+ if params[:starred]
+ projects = projects.merge(current_user.starred_projects)
+ end
+
if params[:search].present?
projects = projects.search(params[:search])
end
@@ -304,7 +312,7 @@ module API
header['X-Sendfile'] = path
body
else
- path
+ file path
end
end
@@ -328,16 +336,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!
@@ -380,14 +389,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 e8975eb57e0..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, '')
@@ -30,7 +30,7 @@ module API
def wiki?
@wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
+ !Project.find_by_full_path(project_path)
end
def project
@@ -41,7 +41,7 @@ module API
# the wiki repository as well.
project_path.chomp!('.wiki') if wiki?
- Project.find_with_namespace(project_path)
+ Project.find_by_full_path(project_path)
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 2199eea7e5f..0764b58fb4c 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -2,7 +2,7 @@ module API
module Helpers
module Pagination
def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ relation.page(params[:page]).per(params[:per_page]).tap do |data|
add_pagination_headers(data)
end
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 00000000000..ec2bcaed929
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -0,0 +1,77 @@
+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 job_not_found!
+ if headers['User-Agent'].to_s =~ /gitlab(-ci-multi)?-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
+ no_content!
+ else
+ not_found!
+ end
+ 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 fe016c1ec0a..1abe8639445 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -10,19 +10,9 @@ module API
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
+ args[:label_name] = args.delete(:labels)
- match_all_labels = args.delete(:match_all_labels)
- labels = args.delete(:labels)
- args[:label_name] = labels if match_all_labels
-
- args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
-
- issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
-
- # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
- if !match_all_labels && labels.present?
- issues = issues.includes(:labels).where('labels.title' => labels.split(','))
- end
+ issues = IssuesFinder.new(current_user, args).execute
issues.reorder(args[:order_by] => args[:sort])
end
@@ -35,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
@@ -50,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',
@@ -60,7 +51,7 @@ 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
@@ -69,7 +60,7 @@ module API
end
resource :groups 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',
@@ -79,9 +70,9 @@ module API
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+ 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
@@ -92,12 +83,11 @@ module API
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',
desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
@@ -105,17 +95,17 @@ module API
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
@@ -126,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
@@ -138,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
@@ -162,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.'
@@ -171,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
@@ -180,9 +166,13 @@ module API
params.delete(:updated_at)
end
+ update_params = declared_params(include_missing: false).merge(request: request, api: true)
+
issue = ::Issues::UpdateService.new(user_project,
current_user,
- declared_params(include_missing: false)).execute(issue)
+ update_params).execute(issue)
+
+ render_spam_error! if issue.spam?
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
@@ -195,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])
@@ -215,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..44118522abe
--- /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 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 652786d4e3e..59f0e7cb647 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,6 +1,7 @@
module API
- # Labels API
class Labels < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -10,8 +11,11 @@ module API
desc 'Get all labels of the project' do
success Entities::Label
end
+ params do
+ use :pagination
+ end
get ':id/labels' do
- present available_labels, with: Entities::Label, current_user: current_user, project: user_project
+ present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
@@ -52,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 d85f1f78cd6..baf85e6075a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -55,24 +55,13 @@ module API
authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id])
+ conflict!('Member already exists') if member
- # We need this explicit check because `source.add_user` doesn't
- # currently return the member created so it would return 201 even if
- # the member already existed...
- # The `source_type == 'group'` check is to ensure back-compatibility
- # but 409 behavior should be used for both project and group members in 9.0!
- conflict!('Member already exists') if source_type == 'group' && member
-
- unless member
- member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- end
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
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
@@ -86,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
@@ -108,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 bc3d69f6904..a59e39cca26 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -1,6 +1,8 @@
module API
# MergeRequestDiff API
class MergeRequestDiffs < Grape::API
+ include PaginationParams
+
before { authenticate! }
resource :projects do
@@ -11,13 +13,13 @@ module API
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_iid/versions" do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- 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: Entities::MergeRequestDiff
+ present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
@@ -27,12 +29,12 @@ module API
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 7ffb38e62da..7a03955a045 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,8 +2,6 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
-
before { authenticate! }
params do
@@ -27,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'
@@ -37,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',
@@ -46,14 +52,14 @@ module API
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
- optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+ merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
merge_requests =
case params[:state]
@@ -64,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
@@ -95,186 +101,177 @@ 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
- # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "merge_requests/:merge_request_id/..." instead.
- #
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
- { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
- desc 'Get a single merge request' do
- if status == :deprecated
- detail DEPRECATION_MESSAGE
- end
- success Entities::MergeRequest
- end
- get path do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
-
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- end
+ desc 'Get a single merge request' do
+ success Entities::MergeRequest
+ end
+ get ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
- end
- get "#{path}/commits" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- present merge_request.commits, with: Entities::RepoCommit
- end
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
+ 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)
- desc 'Show the merge request changes' do
- success Entities::MergeRequestChanges
- end
- get "#{path}/changes" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(commits), with: Entities::RepoCommit
+ end
- present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
- end
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
+ get ':id/merge_requests/:merge_request_iid/changes' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
- desc 'Update a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
- optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
- optional :state_event, type: String, values: %w[close reopen merge],
- desc: 'Status of the merge request'
- use :optional_params
- at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event,
- :remove_source_branch
- end
- put path do
- merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
+ end
- 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?
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(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?
- if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
- end
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
- desc 'Merge a merge request' do
- success 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'
- optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ if merge_request.valid?
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
end
- put "#{path}/merge" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ end
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
+ desc 'Merge a merge request' do
+ success 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_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_iid/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
- not_allowed! unless merge_request.mergeable_state?
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+ not_allowed! unless merge_request.mergeable_state?
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- else
- ::MergeRequests::MergeService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- end
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ 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)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
end
- desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
- success Entities::MergeRequest
- end
- post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
+ post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
- ::MergeRequest::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
- end
+ unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- desc 'Get the comments of a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/comments" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
- end
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
- desc 'Post a comment to a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- requires :note, type: String, desc: 'The text of the comment'
- end
- post "#{path}/comments" do
- merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+ desc 'Get the comments of a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ 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
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
+ desc 'Post a comment to a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post ':id/merge_requests/:merge_request_iid/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note)
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- desc 'List issues that will be closed on merge' do
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/closes_issues" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ if note.save
+ present note, with: Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
+
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ 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
end
end
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 3c373a84ec5..abd263c1dfc 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -30,7 +30,8 @@ module API
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,11 +116,38 @@ 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::MergeRequestBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/merge_requests' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_id: milestone.id,
+ sort: 'position_asc'
+ }
+
+ merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
+ 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 4d2a8f48267..3b3e45cbd06 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -4,7 +4,7 @@ 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'
@@ -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
@@ -131,9 +131,7 @@ module API
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
- ::Notes::DeleteService.new(user_project, current_user).execute(note)
-
- present note, with: Entities::Note
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
end
end
end
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
index 8c1e4381a74..f566eb3ed2b 100644
--- a/lib/api/pagination_params.rb
+++ b/lib/api/pagination_params.rb
@@ -15,8 +15,8 @@ module API
included do
helpers do
params :pagination do
- optional :page, type: Integer, desc: 'Current page number'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ optional :page, type: Integer, default: 1, desc: 'Current page number'
+ optional :per_page, type: Integer, default: 20, desc: 'Number of items per page'
end
end
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index b634b1d0222..0721b975ba4 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -10,20 +10,20 @@ module API
resource :projects 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
detail 'This feature was introduced in GitLab 8.14'
success Entities::Pipeline
@@ -58,7 +58,7 @@ module API
present pipeline, with: Entities::Pipeline
end
- desc 'Retry failed builds in the pipeline' do
+ desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index cb679e6658a..57a5f97dc7f 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -32,9 +32,7 @@ module API
use :pagination
end
get ":id/hooks" do
- hooks = paginate user_project.hooks
-
- present hooks, with: Entities::ProjectHook
+ present paginate(user_project.hooks), with: Entities::ProjectHook
end
desc 'Get a project hook' do
@@ -92,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 dcc0c82ee27..f57e7ea4032 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -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
@@ -63,6 +61,8 @@ module API
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::ProjectSnippet
else
@@ -78,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
@@ -92,12 +90,16 @@ module API
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
+ .merge(request: request, api: true)
+
snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
UpdateSnippetService.new(user_project, current_user, snippet,
snippet_params).execute
- if snippet.persisted?
+ render_spam_error! if snippet.spam?
+
+ if snippet.valid?
present snippet, with: Entities::ProjectSnippet
else
render_validation_error!(snippet)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 941f47114a4..63a4cdd5954 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,26 +16,12 @@ 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 :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
- 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
-
- def map_public_to_visibility_level(attrs)
- publik = attrs.delete(:public)
- if !publik.nil? && !attrs[:visibility_level].present?
- # Since setting the public attribute to private could mean either
- # private or internal, use the more conservative option, private.
- attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
- end
- attrs
- end
end
resource :projects do
@@ -58,9 +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,91 +82,23 @@ module API
params do
use :collection_params
end
- get '/visible' do
- entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present_projects ProjectsFinder.new.execute(current_user), with: entity
- end
-
- desc 'Get a projects list for authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
get do
- authenticate!
-
- present_projects current_user.authorized_projects,
- with: Entities::ProjectWithAccess
- end
-
- desc 'Get an owned projects list for authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/owned' do
- authenticate!
-
- present_projects current_user.owned_projects,
- with: Entities::ProjectWithAccess,
- statistics: params[:statistics]
- end
-
- desc 'Gets starred project for the authenticated user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- end
- get '/starred' do
- authenticate!
-
- present_projects current_user.viewable_starred_projects
- end
-
- desc 'Get all projects for admin user' do
- success Entities::BasicProjectDetails
- end
- params do
- use :collection_params
- use :statistics_params
- end
- get '/all' do
- authenticated_as_admin!
-
- present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
- end
-
- desc 'Search for projects the current user has access to' do
- success Entities::Project
- end
- params do
- requires :query, type: String, desc: 'The project name to be searched'
- use :sort_params
- use :pagination
- end
- get "/search/:query", requirements: { query: /[^\/]+/ } do
- search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
- projects = search_service.objects('projects', params[:page])
- projects = projects.reorder(params[:order_by] => params[:sort])
-
- present paginate(projects), with: Entities::Project
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics]
end
desc 'Create new project' do
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
post do
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -206,7 +127,7 @@ module API
user = User.find_by(id: params.delete(:user_id))
not_found!('User') unless user
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
@@ -247,7 +168,7 @@ module API
params do
optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
end
- post 'fork/:id' do
+ post ':id/fork' do
fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace]
@@ -284,16 +205,16 @@ 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, :public, :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
put ':id' do
authorize_admin_project
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ 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
@@ -344,7 +265,7 @@ module API
desc 'Unstar a project' do
success Entities::Project
end
- delete ':id/star' do
+ post ':id/unstar' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
@@ -359,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'
@@ -428,7 +351,6 @@ module API
not_found!('Group Link') unless link
link.destroy
- no_content!
end
desc 'Upload a file'
@@ -452,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 4ca6646a6f1..531ef5a63ea 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -2,6 +2,8 @@ require 'mime/types'
module API
class Repositories < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
@@ -15,61 +17,67 @@ 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)
not_found!('Tree') unless commit
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
-
- present tree.sorted_entries, with: Entities::RepoTreeObject
+ entries = ::Kaminari.paginate_array(tree.sorted_entries)
+ 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
-
- send_git_blob repo, blob
+ get ':id/repository/blobs/:sha' do
+ assign_blob_vars!
+
+ {
+ size: @blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(@blob.data),
+ sha: @blob.id
+ }
end
desc 'Get an archive of the repository'
@@ -100,10 +108,13 @@ module API
desc 'Get repository contributors' do
success Entities::Contributor
end
+ params do
+ use :pagination
+ end
get ':id/repository/contributors' do
begin
- present user_project.repository.contributors,
- with: Entities::Contributor
+ contributors = ::Kaminari.paginate_array(user_project.repository.contributors)
+ present paginate(contributors), with: Entities::Contributor
rescue
not_found!
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 00000000000..c700d2ef4a1
--- /dev/null
+++ b/lib/api/runner.rb
@@ -0,0 +1,250 @@
+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
+ end
+
+ resource :jobs do
+ desc 'Request a job' do
+ success Entities::JobRequest::Response
+ 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!
+ not_found! 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 job_not_found!
+ 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
+ job_not_found!
+ 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 4816b5ed1b7..2e41f16f8c6 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
@@ -60,8 +60,9 @@ module API
put ':id' do
runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner)
+ update_service = Ci::UpdateRunnerService.new(runner)
- if runner.update(declared_params(include_missing: false))
+ if update_service.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
@@ -77,9 +78,8 @@ 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
@@ -135,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..5aa2f5eba7b 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -122,9 +122,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_jobs,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken jobs'
}
],
'campfire' => [
@@ -403,9 +403,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_jobs,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken jobs'
}
],
'pivotaltracker' => [
@@ -422,6 +422,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,
@@ -558,12 +566,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' => [
@@ -598,7 +620,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 +663,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
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c5eff16a5de..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,9 @@ 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'
given metrics_enabled: ->(val) { val } do
@@ -107,6 +109,7 @@ module API
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,
@@ -115,15 +118,19 @@ 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, :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,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled
+ :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 eb9ece49e7f..b93fdc62808 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -58,15 +58,17 @@ 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)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -83,19 +85,22 @@ 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))
return not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -113,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 e11d7537cc9..772b5cca017 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) },
@@ -21,7 +20,7 @@ module API
desc 'Subscribe to a resource' do
success entity_class
end
- post ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/subscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
@@ -35,7 +34,7 @@ module API
desc 'Unsubscribe from a resource' do
success entity_class
end
- delete ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/unsubscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 708ec8cfe70..ed7b23b474a 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -1,6 +1,7 @@
module API
- # Hooks API
class SystemHooks < Grape::API
+ include PaginationParams
+
before do
authenticate!
authenticated_as_admin!
@@ -10,10 +11,11 @@ module API
desc 'Get the list of system hooks' do
success Entities::Hook
end
+ params do
+ use :pagination
+ end
get do
- hooks = SystemHook.all
-
- present hooks, with: Entities::Hook
+ present paginate(SystemHook.all), with: Entities::Hook
end
desc 'Create a new system hook' do
@@ -64,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 5b345db3a41..d31ef9de26b 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,6 +1,7 @@
module API
- # Git Tags API
class Tags < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
@@ -10,9 +11,12 @@ module API
desc 'Get a project repository tags' do
success Entities::RepoTag
end
+ params do
+ use :pagination
+ end
get ":id/repository/tags" do
- present user_project.repository.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
+ tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
+ present paginate(tags), with: Entities::RepoTag, project: user_project
end
desc 'Get a single repository tag' do
@@ -40,7 +44,7 @@ module API
post ':id/repository/tags' do
authorize_push_project
- result = CreateTagService.new(user_project, current_user).
+ result = ::Tags::CreateService.new(user_project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@@ -59,14 +63,10 @@ module API
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
- result = DeleteTagService.new(user_project, current_user).
+ 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/templates.rb b/lib/api/templates.rb
index e23f99256a5..0fc13b35d5b 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,5 +1,7 @@
module API
class Templates < Grape::API
+ include PaginationParams
+
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
klass: Gitlab::Template::GitignoreTemplate,
@@ -24,7 +26,6 @@ module API
/[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]/xi.freeze
- DEPRECATION_MESSAGE = ' This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
helpers do
def parsed_license_template
@@ -46,74 +47,64 @@ module API
end
end
- { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
- desc 'Get the list of the available license template' do
- detailed_desc = 'This feature was introduced in GitLab 8.7.'
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success Entities::RepoLicense
- end
- params do
- optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
- end
- get route do
- options = {
- featured: declared(params).popular.present? ? true : nil
- }
- present Licensee::License.all(options), with: Entities::RepoLicense
- end
+ desc 'Get the list of the available license template' do
+ detail 'This feature was introduced in GitLab 8.7.'
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ use :pagination
+ end
+ get "templates/licenses" do
+ options = {
+ featured: declared(params).popular.present? ? true : nil
+ }
+ licences = ::Kaminari.paginate_array(Licensee::License.all(options))
+ present paginate(licences), with: Entities::RepoLicense
end
- { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
- desc 'Get the text for a specific license' do
- detailed_desc = 'This feature was introduced in GitLab 8.7.'
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success Entities::RepoLicense
- end
- params do
- requires :name, type: String, desc: 'The name of the template'
- end
- get route, requirements: { name: /[\w\.-]+/ } do
- not_found!('License') unless Licensee::License.find(declared(params).name)
+ desc 'Get the text for a specific license' do
+ detail 'This feature was introduced in GitLab 8.7.'
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
+ not_found!('License') unless Licensee::License.find(declared(params).name)
- template = parsed_license_template
+ template = parsed_license_template
- present template, with: Entities::RepoLicense
- end
+ present template, with: ::API::Entities::RepoLicense
end
GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
klass = properties[:klass]
gitlab_version = properties[:gitlab_version]
- { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
- desc 'Get the list of the available template' do
- detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success Entities::TemplatesList
- end
- get route do
- present klass.all, with: Entities::TemplatesList
- end
+ desc 'Get the list of the available template' do
+ detail "This feature was introduced in GitLab #{gitlab_version}."
+ success Entities::TemplatesList
+ end
+ params do
+ use :pagination
+ end
+ get "templates/#{template_type}" do
+ templates = ::Kaminari.paginate_array(klass.all)
+ present paginate(templates), with: Entities::TemplatesList
end
- { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
- desc 'Get the text for a specific template present in local filesystem' do
- detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
- detailed_desc << DEPRECATION_MESSAGE unless status == :ok
- detail detailed_desc
- success Entities::Template
- end
- params do
- requires :name, type: String, desc: 'The name of the template'
- end
- get route do
- new_template = klass.find(declared(params).name)
+ desc 'Get the text for a specific template present in local filesystem' do
+ detail "This feature was introduced in GitLab #{gitlab_version}."
+ success Entities::Template
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get "templates/#{template_type}/:name" do
+ new_template = klass.find(declared(params).name)
- render_response(template_type, new_template)
- end
+ render_response(template_type, new_template)
end
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 9bd077263a7..d9b8837a5bb 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
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)
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end
- delete ':id' do
+ post ':id/mark_as_done' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,9 +66,11 @@ module API
end
desc 'Mark all todos as done'
- delete do
+ post '/mark_as_done' do
todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user)
+
+ no_content!
end
end
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 87a717ba751..119e9024712 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -6,37 +6,32 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
- desc 'Trigger a GitLab project build' do
- success Entities::TriggerRequest
+ 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" 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 11a7368b4c0..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
@@ -82,7 +87,9 @@ module API
end
params do
requires :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the new user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user'
use :optional_attributes
@@ -94,8 +101,18 @@ module API
user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid)
confirm = user_params.delete(:confirm)
+ user = User.new(user_params.except(:extern_uid, :provider, :reset_password))
+
+ if user_params.delete(:reset_password)
+ user.attributes = {
+ force_random_password: true,
+ password_expires_at: nil,
+ created_by_id: current_user.id
+ }
+ user.generate_password
+ user.generate_reset_token
+ end
- user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
if identity_attrs.any?
@@ -160,6 +177,8 @@ module API
end
end
+ 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
else
@@ -195,6 +214,7 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/keys' do
authenticated_as_admin!
@@ -202,7 +222,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- present user.keys, with: Entities::SSHKey
+ present paginate(user.keys), with: Entities::SSHKey
end
desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
@@ -221,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
@@ -252,13 +272,14 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/emails' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
- present user.emails, with: Entities::Email
+ present paginate(user.emails), with: Entities::Email
end
desc 'Delete an email address of a specified user. Available only for admins.' do
@@ -291,14 +312,14 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- DeleteUserService.new(current_user).execute(user)
+ ::Users::DestroyService.new(current_user).execute(user)
end
desc 'Block a user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- put ':id/block' do
+ post ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -314,7 +335,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- put ':id/unblock' do
+ post ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -346,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
@@ -359,8 +450,11 @@ module API
desc "Get the currently authenticated user's SSH keys" do
success Entities::SSHKey
end
+ params do
+ use :pagination
+ end
get "keys" do
- present current_user.keys, with: Entities::SSHKey
+ present paginate(current_user.keys), with: Entities::SSHKey
end
desc 'Get a single key owned by currently authenticated user' do
@@ -403,14 +497,17 @@ 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
success Entities::Email
end
+ params do
+ use :pagination
+ end
get "emails" do
- present current_user.emails, with: Entities::Email
+ present paginate(current_user.emails), with: Entities::Email
end
desc 'Get a single email address owned by the currently authenticated user' do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
new file mode 100644
index 00000000000..cf9e1551f60
--- /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 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
new file mode 100644
index 00000000000..b1c2a3c59f2
--- /dev/null
+++ b/lib/api/v3/boards.rb
@@ -0,0 +1,72 @@
+module API
+ module V3
+ class Boards < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get all project boards' do
+ detail 'This feature was introduced in 8.13'
+ success ::API::Entities::Board
+ end
+ get ':id/boards' do
+ authorize!(:read_board, user_project)
+ present user_project.boards, with: ::API::Entities::Board
+ end
+
+ params do
+ requires :board_id, type: Integer, desc: 'The ID of a board'
+ end
+ segment ':id/boards/:board_id' do
+ helpers do
+ def project_board
+ board = user_project.boards.first
+
+ if params[:board_id] == board.id
+ board
+ else
+ not_found!('Board')
+ end
+ end
+
+ def board_lists
+ project_board.lists.destroyable
+ end
+ end
+
+ desc 'Get the lists of a project board' do
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
+ success ::API::Entities::List
+ end
+ get '/lists' do
+ 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
+ end
+end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
new file mode 100644
index 00000000000..699e41b5537
--- /dev/null
+++ b/lib/api/v3/branches.rb
@@ -0,0 +1,51 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Branches < Grape::API
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a project repository branches' do
+ success ::API::Entities::RepoBranch
+ end
+ get ":id/repository/branches" do
+ branches = user_project.repository.branches.sort_by(&:name)
+
+ 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
+ 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
new file mode 100644
index 00000000000..d254d247042
--- /dev/null
+++ b/lib/api/v3/commits.rb
@@ -0,0 +1,196 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Commits < 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
+ desc 'Get a project repository commits' do
+ success ::API::Entities::RepoCommit
+ 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 :since, type: DateTime, desc: 'Only commits after or in this date will be returned'
+ optional :until, type: DateTime, desc: 'Only commits before or in 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'
+ end
+ get ":id/repository/commits" do
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ offset = params[:page] * params[:per_page]
+
+ commits = user_project.repository.commits(ref,
+ path: params[:path],
+ limit: params[:per_page],
+ offset: offset,
+ after: params[:since],
+ before: params[:until])
+
+ present commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Commit multiple file changes as one commit' do
+ success ::API::Entities::RepoCommitDetail
+ detail 'This feature was introduced in GitLab 8.13'
+ end
+ params do
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit message'
+ requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
+ optional :author_email, type: String, desc: 'Author email for commit'
+ optional :author_name, type: String, desc: 'Author name for commit'
+ end
+ post ":id/repository/commits" do
+ authorize! :push_code, user_project
+
+ attrs = declared_params.dup
+ branch = attrs.delete(:branch_name)
+ attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
+
+ result = ::Files::MultiService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ present commit_detail, with: ::API::Entities::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Get a specific commit of a project' do
+ success ::API::Entities::RepoCommitDetail
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ":id/repository/commits/:sha" do
+ commit = user_project.commit(params[:sha])
+
+ not_found! "Commit" unless commit
+
+ present commit, with: ::API::Entities::RepoCommitDetail
+ end
+
+ desc 'Get the diff for a specific commit of a project' do
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ":id/repository/commits/:sha/diff" do
+ commit = user_project.commit(params[:sha])
+
+ not_found! "Commit" unless commit
+
+ commit.raw_diffs.to_a
+ end
+
+ desc "Get a commit's comments" do
+ success ::API::Entities::CommitNote
+ failure [[404, 'Not Found']]
+ end
+ params do
+ use :pagination
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ':id/repository/commits/:sha/comments' do
+ commit = user_project.commit(params[:sha])
+
+ not_found! 'Commit' unless commit
+ notes = Note.where(commit_id: commit.id).order(:created_at)
+
+ present paginate(notes), with: ::API::Entities::CommitNote
+ end
+
+ desc 'Cherry pick commit into a branch' do
+ detail 'This feature was introduced in GitLab 8.15'
+ success ::API::Entities::RepoCommit
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ post ':id/repository/commits/:sha/cherry_pick' do
+ authorize! :push_code, user_project
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit_params = {
+ commit: commit,
+ start_branch: params[:branch],
+ target_branch: params[:branch]
+ }
+
+ result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ branch = user_project.repository.find_branch(params[:branch])
+ present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Post comment to commit' do
+ success ::API::Entities::CommitNote
+ end
+ params do
+ requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
+ requires :note, type: String, desc: 'The text of the comment'
+ 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: %w(new old), default: 'new', desc: 'The type of the line'
+ end
+ end
+ post ':id/repository/commits/:sha/comments' do
+ commit = user_project.commit(params[:sha])
+ not_found! 'Commit' unless commit
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'Commit',
+ commit_id: commit.id
+ }
+
+ if params[:path]
+ commit.raw_diffs(all_diffs: true).each do |diff|
+ next unless diff.new_path == params[:path]
+ lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
+
+ lines.each do |line|
+ next unless line.new_pos == params[:line] && line.type == params[:line_type]
+ break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+ end
+
+ break if opts[:line_code]
+ end
+
+ opts[:type] = LegacyDiffNote.name if opts[:line_code]
+ end
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::CommitNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
new file mode 100644
index 00000000000..5bbb167755c
--- /dev/null
+++ b/lib/api/v3/deploy_keys.rb
@@ -0,0 +1,122 @@
+module API
+ module V3
+ class DeployKeys < Grape::API
+ before { authenticate! }
+
+ get "deploy_keys" do
+ authenticated_as_admin!
+
+ keys = DeployKey.all
+ present keys, with: ::API::Entities::SSHKey
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of the project'
+ end
+ resource :projects do
+ before { authorize_admin_project }
+
+ %w(keys deploy_keys).each do |path|
+ desc "Get a specific project's deploy keys" do
+ success ::API::Entities::SSHKey
+ end
+ get ":id/#{path}" do
+ present user_project.deploy_keys, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get single deploy key' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/#{path}" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/#{path}/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
+
+ if key
+ present key, with: ::API::Entities::SSHKey
+ else
+ not_found!('Deploy Key')
+ end
+ end
+
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
+
+ present key.deploy_key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
+ key.destroy
+ else
+ not_found!('Deploy Key')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 00000000000..95114ad1fe1
--- /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 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
new file mode 100644
index 00000000000..832b4bdeb4f
--- /dev/null
+++ b/lib/api/v3/entities.rb
@@ -0,0 +1,253 @@
+module API
+ module V3
+ module Entities
+ class ProjectSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: ::API::Entities::UserBasic
+ expose :updated_at, :created_at
+ expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ 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..3056b70e6ef
--- /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 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
new file mode 100644
index 00000000000..4f8d58d37c8
--- /dev/null
+++ b/lib/api/v3/files.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Files < Grape::API
+ helpers do
+ def commit_params(attrs)
+ {
+ file_path: attrs[:file_path],
+ start_branch: attrs[:branch],
+ target_branch: attrs[:branch],
+ commit_message: attrs[:commit_message],
+ file_content: attrs[:content],
+ file_content_encoding: attrs[:encoding],
+ author_email: attrs[:author_email],
+ author_name: attrs[:author_name]
+ }
+ end
+
+ def commit_response(attrs)
+ {
+ file_path: attrs[:file_path],
+ branch: attrs[:branch]
+ }
+ end
+
+ params :simple_file_params do
+ requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+ requires :branch_name, 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'
+ optional :author_name, type: String, desc: 'The name of the author'
+ end
+
+ params :extended_file_params do
+ use :simple_file_params
+ requires :content, type: String, desc: 'File content'
+ optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get a file from 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'
+ end
+ get ":id/repository/files" do
+ 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)
+ status(200)
+
+ {
+ file_name: blob.name,
+ file_path: blob.path,
+ size: blob.size,
+ encoding: "base64",
+ 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])
+ }
+ end
+
+ desc 'Create new file in repository'
+ params do
+ use :extended_file_params
+ end
+ post ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(201)
+ commit_response(file_params)
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Update existing file in repository'
+ params do
+ use :extended_file_params
+ end
+ put ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(200)
+ commit_response(file_params)
+ else
+ http_status = result[:http_status] || 400
+ render_api_error!(result[:message], http_status)
+ end
+ end
+
+ desc 'Delete an existing file in repository'
+ params do
+ use :simple_file_params
+ end
+ delete ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(200)
+ commit_response(file_params)
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
new file mode 100644
index 00000000000..0aad87a3f58
--- /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 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" 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
new file mode 100644
index 00000000000..258cbfed022
--- /dev/null
+++ b/lib/api/v3/issues.rb
@@ -0,0 +1,231 @@
+module API
+ module V3
+ class Issues < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def find_issues(args = {})
+ args = params.merge(args)
+
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ # IssuesFinder expects iids
+ args[:iids] = args.delete(:iid) if args.key?(:iid)
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
+
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
+ end
+
+ params :issues_params do
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
+ 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'
+ use :pagination
+ end
+
+ params :issue_params do
+ optional :description, type: String, desc: 'The description of an issue'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
+ optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ end
+ end
+
+ resource :issues do
+ desc "Get currently authenticated user's issues" do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get do
+ issues = find_issues(scope: 'authored')
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Get a list of group issues' do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get ":id/issues" do
+ group = find_group!(params[:id])
+
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ desc 'Get a list of project issues' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
+ use :issues_params
+ end
+ get ":id/issues" do
+ project = find_project(params[:id])
+
+ issues = find_issues(project_id: project.id)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Get a single project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ get ":id/issues/:issue_id" do
+ issue = find_project_issue(params[:issue_id])
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a new project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ 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,
+ desc: 'The IID of a merge request for which to resolve discussions'
+ use :issue_params
+ end
+ post ':id/issues' do
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:created_at)
+ end
+
+ issue_params = declared_params(include_missing: false)
+ 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,
+ issue_params.merge(request: request, api: true)).execute
+ render_spam_error! if issue.spam?
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Update an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The 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.'
+ optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
+ use :issue_params
+ 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))
+ authorize! :update_issue, issue
+
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:updated_at)
+ end
+
+ update_params = declared_params(include_missing: false).merge(request: request, api: true)
+
+ issue = ::Issues::UpdateService.new(user_project,
+ current_user,
+ update_params).execute(issue)
+
+ render_spam_error! if issue.spam?
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Move an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The 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])
+ not_found!('Issue') unless issue
+
+ new_project = Project.find_by(id: params[:to_project_id])
+ not_found!('Project') unless new_project
+
+ begin
+ issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ rescue ::Issues::MoveService::MoveError => error
+ render_api_error!(error.message, 400)
+ end
+ end
+
+ desc 'Delete a project issue'
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ delete ":id/issues/:issue_id" do
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
+
+ authorize!(:destroy_issue, issue)
+
+ status(200)
+ issue.destroy
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
new file mode 100644
index 00000000000..41f45d244e3
--- /dev/null
+++ b/lib/api/v3/labels.rb
@@ -0,0 +1,34 @@
+module API
+ module V3
+ class Labels < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects 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
+end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
new file mode 100644
index 00000000000..3d4972afd9d
--- /dev/null
+++ b/lib/api/v3/members.rb
@@ -0,0 +1,134 @@
+module API
+ module V3
+ class Members < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers ::API::Helpers::MembersHelpers
+
+ %w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
+ resource source_type.pluralize do
+ desc 'Gets a list of group or project members viewable by the authenticated user.' do
+ success ::API::Entities::Member
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ use :pagination
+ end
+ get ":id/members" do
+ source = find_source(source_type, params[:id])
+
+ users = source.users
+ users = users.merge(User.search(params[:query])) if params[:query]
+
+ present paginate(users), with: ::API::Entities::Member, source: source
+ end
+
+ desc 'Gets a member of a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
+ get ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+
+ members = source.members
+ member = members.find_by!(user_id: params[:user_id])
+
+ present member.user, with: ::API::Entities::Member, member: member
+ end
+
+ desc 'Adds a member to a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
+ post ":id/members" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+
+ member = source.members.find_by(user_id: params[:user_id])
+
+ # We need this explicit check because `source.add_user` doesn't
+ # currently return the member created so it would return 201 even if
+ # the member already existed...
+ # The `source_type == 'group'` check is to ensure back-compatibility
+ # but 409 behavior should be used for both project and group members in 9.0!
+ conflict!('Member already exists') if source_type == 'group' && member
+
+ unless member
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ end
+ if member.persisted? && member.valid?
+ present member.user, with: ::API::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
+
+ desc 'Updates a member of a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level'
+ 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.delete(:id))
+ authorize_admin_source!(source_type, source)
+
+ member = source.members.find_by!(user_id: params.delete(:user_id))
+
+ 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
+ # 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
+
+ desc 'Removes a user from a group or project.'
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
+ delete ":id/members/:user_id" do
+ source = find_source(source_type, params[: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?
+ status(200 )
+ { message: "Access revoked", id: params[:user_id].to_i }
+ else
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
+
+ present member.user, with: ::API::Entities::Member, member: member
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 00000000000..a462803e26c
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,43 @@
+module API
+ module V3
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ resource :projects 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 :id, type: String, desc: 'The ID of a project'
+ 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 :id, type: String, desc: 'The ID of a project'
+ 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
new file mode 100644
index 00000000000..7dbd4691a94
--- /dev/null
+++ b/lib/api/v3/merge_requests.rb
@@ -0,0 +1,290 @@
+module API
+ module V3
+ class MergeRequests < Grape::API
+ include PaginationParams
+
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ helpers do
+ def handle_merge_request_errors!(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ elsif errors[:branch_conflict].any?
+ error!(errors[:branch_conflict], 422)
+ elsif errors[:validate_fork].any?
+ error!(errors[:validate_fork], 422)
+ elsif errors[:validate_branches].any?
+ conflict!(errors[:validate_branches])
+ end
+
+ 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'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
+ end
+ end
+
+ desc 'List merge requests' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ use :pagination
+ end
+ get ":id/merge_requests" do
+ authorize! :read_merge_request, user_project
+
+ merge_requests = user_project.merge_requests.inc_notes_with_associations
+ merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+
+ merge_requests =
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
+ else merge_requests
+ end
+
+ merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
+ 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::V3::Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
+ post ":id/merge_requests" do
+ authorize! :create_merge_request, user_project
+
+ 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?
+
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
+
+ if merge_request.valid?
+ 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 'Delete a merge request'
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ delete ":id/merge_requests/:merge_request_id" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ authorize!(:destroy_merge_request, merge_request)
+
+ status(200)
+ merge_request.destroy
+ end
+
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ 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::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Get the commits of a merge request' do
+ success ::API::Entities::RepoCommit
+ end
+ get "#{path}/commits" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Show the merge request changes' do
+ success ::API::Entities::MergeRequestChanges
+ end
+ get "#{path}/changes" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user
+ end
+
+ desc 'Update a merge request' do
+ success ::API::V3::Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put path do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :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?
+
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+
+ if merge_request.valid?
+ 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::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 build succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put "#{path}/merge" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
+
+ not_allowed! unless merge_request.mergeable_state?
+
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ end
+
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ 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_pipeline_succeeds?(current_user)
+
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
+
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote
+ end
+
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'List issues that will be closed on merge' do
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/closes_issues" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
new file mode 100644
index 00000000000..2a850a08a8a
--- /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 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..0796bb62e68
--- /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 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..2c26a5f7d35
--- /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 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..861b991b8e1
--- /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 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
new file mode 100644
index 00000000000..809ca4f37ba
--- /dev/null
+++ b/lib/api/v3/project_snippets.rb
@@ -0,0 +1,143 @@
+module API
+ module V3
+ class ProjectSnippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ def handle_project_member_errors(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ end
+ not_found!
+ end
+
+ def snippets_for_current_user
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params)
+ end
+ end
+
+ desc 'Get all project snippets' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ use :pagination
+ end
+ get ":id/snippets" do
+ present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Get a single project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find(params[:snippet_id])
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Create a new project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ 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'
+ end
+ post ":id/snippets" do
+ authorize! :create_project_snippet, user_project
+ snippet_params = declared_params.merge(request: request, api: true)
+ snippet_params[:content] = snippet_params.delete(:code)
+
+ snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+
+ render_spam_error! if snippet.spam?
+
+ if snippet.persisted?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ 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'
+ at_least_one_of :title, :file_name, :code, :visibility_level
+ end
+ put ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
+ not_found!('Snippet') unless snippet
+
+ authorize! :update_project_snippet, snippet
+
+ snippet_params = declared_params(include_missing: false)
+ .merge(request: request, api: true)
+
+ snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
+
+ UpdateSnippetService.new(user_project, current_user, snippet,
+ snippet_params).execute
+
+ render_spam_error! if snippet.spam?
+
+ if snippet.valid?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Delete a project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ delete ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ authorize! :admin_project_snippet, snippet
+ snippet.destroy
+
+ status(200)
+ end
+
+ desc 'Get a raw project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ 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/projects.rb b/lib/api/v3/projects.rb
new file mode 100644
index 00000000000..47bfc12035a
--- /dev/null
+++ b/lib/api/v3/projects.rb
@@ -0,0 +1,474 @@
+module API
+ module V3
+ class Projects < Grape::API
+ include PaginationParams
+
+ 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'
+ optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
+ optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
+ optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
+ optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
+ optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
+ 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 :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
+ 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 :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
+
+ def map_public_to_visibility_level(attrs)
+ publik = attrs.delete(:public)
+ if !publik.nil? && !attrs[:visibility_level].present?
+ # Since setting the public attribute to private could mean either
+ # private or internal, use the more conservative option, private.
+ attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
+ 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
+ helpers do
+ params :collection_params do
+ use :sort_params
+ use :filter_params
+ use :pagination
+
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ end
+
+ params :sort_params do
+ 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'
+ end
+
+ params :filter_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'
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ params :create_params do
+ optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
+ optional :import_url, type: String, desc: 'URL from which the project is imported'
+ end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: ::API::V3::Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
+ end
+
+ desc 'Get a list of visible projects for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/visible' do
+ entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
+ end
+
+ desc 'Get a projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get do
+ authenticate!
+
+ present_projects current_user.authorized_projects,
+ with: ::API::V3::Entities::ProjectWithAccess
+ end
+
+ desc 'Get an owned projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/owned' do
+ authenticate!
+
+ present_projects current_user.owned_projects,
+ with: ::API::V3::Entities::ProjectWithAccess,
+ statistics: params[:statistics]
+ end
+
+ desc 'Gets starred project for the authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/starred' do
+ authenticate!
+
+ present_projects current_user.viewable_starred_projects
+ end
+
+ desc 'Get all projects for admin user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/all' do
+ authenticated_as_admin!
+
+ 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::V3::Entities::Project
+ end
+ params do
+ requires :query, type: String, desc: 'The project name to be searched'
+ use :sort_params
+ use :pagination
+ end
+ get "/search/:query", requirements: { query: /[^\/]+/ } do
+ search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
+ projects = search_service.objects('projects', params[:page])
+ projects = projects.reorder(params[:order_by] => params[:sort])
+
+ present paginate(projects), with: ::API::V3::Entities::Project
+ end
+
+ desc 'Create new project' do
+ success ::API::V3::Entities::Project
+ end
+ params do
+ 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
+ post do
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(current_user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::V3::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ if project.errors[:limit_reached].present?
+ error!(project.errors[:limit_reached], 403)
+ end
+ render_validation_error!(project)
+ end
+ end
+
+ desc 'Create new project for a specified user. Only available to admin users.' do
+ success ::API::V3::Entities::Project
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the project'
+ requires :user_id, type: Integer, desc: 'The ID of a user'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ use :optional_params
+ use :create_params
+ end
+ post "user/:user_id" do
+ authenticated_as_admin!
+ user = User.find_by(id: params.delete(:user_id))
+ not_found!('User') unless user
+
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::V3::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ render_validation_error!(project)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: /[^\/]+/ } do
+ desc 'Get a single project' do
+ success ::API::V3::Entities::ProjectWithAccess
+ end
+ get ":id" do
+ 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::V3::Entities::Event
+ end
+ params do
+ use :pagination
+ end
+ get ":id/events" do
+ 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::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'
+ end
+ post 'fork/:id' do
+ fork_params = declared_params(include_missing: false)
+ namespace_id = fork_params[:namespace]
+
+ if namespace_id.present?
+ fork_params[:namespace] = if namespace_id =~ /^\d+$/
+ Namespace.find_by(id: namespace_id)
+ else
+ Namespace.find_by_path_or_name(namespace_id)
+ end
+
+ unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
+ not_found!('Target Namespace')
+ end
+ end
+
+ forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
+
+ if forked_project.errors.any?
+ conflict!(forked_project.errors.messages)
+ else
+ 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::V3::Entities::Project
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the project'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ use :optional_params
+ 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, :public, :visibility_level, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :only_allow_merge_if_all_discussions_are_resolved, :path,
+ :default_branch
+ end
+ put ':id' do
+ authorize_admin_project
+ attrs = map_public_to_visibility_level(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?
+
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ 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)
+ end
+ end
+
+ desc 'Archive a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+
+ desc 'Unarchive a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+
+ desc 'Star a project' do
+ success ::API::V3::Entities::Project
+ end
+ post ':id/star' do
+ if current_user.starred?(user_project)
+ not_modified!
+ else
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::V3::Entities::Project
+ end
+ end
+
+ desc 'Unstar a project' do
+ 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::V3::Entities::Project
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Remove a project'
+ delete ":id" do
+ authorize! :remove_project, user_project
+
+ status(200)
+ ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+ end
+
+ desc 'Mark this project as forked from another'
+ params do
+ requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
+ end
+ post ":id/fork/:forked_from_id" do
+ authenticated_as_admin!
+
+ forked_from_project = find_project!(params[:forked_from_id])
+ not_found!("Source Project") unless forked_from_project
+
+ if user_project.forked_from_project.nil?
+ user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+ else
+ render_api_error!("Project already forked", 409)
+ end
+ end
+
+ desc 'Remove a forked_from relationship'
+ delete ":id/fork" do
+ authorize! :remove_fork_project, user_project
+
+ if user_project.forked?
+ status(200)
+ user_project.forked_project_link.destroy
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Share the project with a group' do
+ success ::API::Entities::ProjectGroupLink
+ end
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of a group'
+ requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
+ optional :expires_at, type: Date, desc: 'Share expiration date'
+ end
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ group = Group.find_by_id(params[:group_id])
+
+ unless group && can?(current_user, :read_group, group)
+ not_found!('Group')
+ end
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new(declared_params(include_missing: false))
+
+ if link.save
+ present link, with: ::API::Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of the group'
+ end
+ delete ":id/share/:group_id" do
+ authorize! :admin_project, user_project
+
+ link = user_project.project_group_links.find_by(group_id: params[:group_id])
+ not_found!('Group Link') unless link
+
+ link.destroy
+ no_content!
+ end
+
+ desc 'Upload a file'
+ params do
+ requires :file, type: File, desc: 'The file to be uploaded'
+ end
+ post ":id/uploads" do
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
+ desc 'Get the users list of a project' do
+ success ::API::Entities::UserBasic
+ end
+ params do
+ optional :search, type: String, desc: 'Return list of users matching the search criteria'
+ use :pagination
+ end
+ get ':id/users' do
+ users = user_project.team.users
+ users = users.search(params[:search]) if params[:search].present?
+
+ present paginate(users), with: ::API::Entities::UserBasic
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
new file mode 100644
index 00000000000..44584e2eb70
--- /dev/null
+++ b/lib/api/v3/repositories.rb
@@ -0,0 +1,109 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Repositories < Grape::API
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ def handle_project_member_errors(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ end
+ not_found!
+ end
+ end
+
+ desc 'Get a project repository tree' do
+ success ::API::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 :path, type: String, desc: 'The path of the tree'
+ optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+ end
+ get ':id/repository/tree' do
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ path = params[:path] || nil
+
+ commit = user_project.commit(ref)
+ not_found!('Tree') unless commit
+
+ tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
+
+ 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
+ get ':id/repository/contributors' do
+ begin
+ present user_project.repository.contributors,
+ with: ::API::Entities::Contributor
+ rescue
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
new file mode 100644
index 00000000000..8967141fe3d
--- /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 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..d77185ffe5a
--- /dev/null
+++ b/lib/api/v3/services.rb
@@ -0,0 +1,641 @@
+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
+
+ resource :projects 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 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
new file mode 100644
index 00000000000..02a4157c26e
--- /dev/null
+++ b/lib/api/v3/subscriptions.rb
@@ -0,0 +1,53 @@
+module API
+ module V3
+ class Subscriptions < Grape::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) },
+ }
+
+ params do
+ 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
+ subscribable_types.each do |type, finder|
+ type_singularized = type.singularize
+ entity_class = ::API::Entities.const_get(type_singularized.camelcase)
+
+ desc 'Subscribe to a resource' do
+ success entity_class
+ end
+ post ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
+
+ if resource.subscribed?(current_user, user_project)
+ not_modified!
+ else
+ resource.subscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
+ end
+ end
+
+ desc 'Unsubscribe from a resource' do
+ success entity_class
+ end
+ delete ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
+
+ if !resource.subscribed?(current_user, user_project)
+ not_modified!
+ else
+ resource.unsubscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
new file mode 100644
index 00000000000..5787c06fc12
--- /dev/null
+++ b/lib/api/v3/system_hooks.rb
@@ -0,0 +1,32 @@
+module API
+ module V3
+ class SystemHooks < Grape::API
+ before do
+ authenticate!
+ authenticated_as_admin!
+ end
+
+ resource :hooks do
+ desc 'Get the list of system hooks' do
+ success ::API::Entities::Hook
+ end
+ 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
+end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
new file mode 100644
index 00000000000..6913720d9c5
--- /dev/null
+++ b/lib/api/v3/tags.rb
@@ -0,0 +1,40 @@
+module API
+ module V3
+ class Tags < Grape::API
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a project repository tags' do
+ success ::API::Entities::RepoTag
+ end
+ get ":id/repository/tags" do
+ 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
+end
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
new file mode 100644
index 00000000000..4c577a8d2b7
--- /dev/null
+++ b/lib/api/v3/templates.rb
@@ -0,0 +1,122 @@
+module API
+ module V3
+ class Templates < Grape::API
+ GLOBAL_TEMPLATE_TYPES = {
+ gitignores: {
+ klass: Gitlab::Template::GitignoreTemplate,
+ gitlab_version: 8.8
+ },
+ gitlab_ci_ymls: {
+ klass: Gitlab::Template::GitlabCiYmlTemplate,
+ gitlab_version: 8.9
+ },
+ dockerfiles: {
+ klass: Gitlab::Template::DockerfileTemplate,
+ gitlab_version: 8.15
+ }
+ }.freeze
+ PROJECT_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
+ DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze
+
+ helpers do
+ def parsed_license_template
+ # We create a fresh Licensee::License object since we'll modify its
+ # content in place below.
+ template = Licensee::License.new(params[:name])
+
+ template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+ template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+ fullname = params[:fullname].presence || current_user.try(:name)
+ template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+ template
+ end
+
+ def render_response(template_type, template)
+ not_found!(template_type.to_s.singularize) unless template
+ present template, with: ::API::Entities::Template
+ end
+ end
+
+ { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status|
+ desc 'Get the list of the available license template' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ end
+ get route do
+ options = {
+ featured: declared(params).popular.present? ? true : nil
+ }
+ present Licensee::License.all(options), with: ::API::Entities::RepoLicense
+ end
+ end
+
+ { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific license' do
+ detailed_desc = 'This feature was introduced in GitLab 8.7.'
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success ::API::Entities::RepoLicense
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route, requirements: { name: /[\w\.-]+/ } do
+ not_found!('License') unless Licensee::License.find(declared(params).name)
+
+ template = parsed_license_template
+
+ present template, with: ::API::Entities::RepoLicense
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
+ klass = properties[:klass]
+ gitlab_version = properties[:gitlab_version]
+
+ { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status|
+ desc 'Get the list of the available template' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success ::API::Entities::TemplatesList
+ end
+ get route do
+ present klass.all, with: ::API::Entities::TemplatesList
+ end
+ end
+
+ { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status|
+ desc 'Get the text for a specific template present in local filesystem' do
+ detailed_desc = "This feature was introduced in GitLab #{gitlab_version}."
+ detailed_desc << DEPRECATION_MESSAGE unless status == :ok
+ detail detailed_desc
+ success ::API::Entities::Template
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the template'
+ end
+ get route do
+ new_template = klass.find(declared(params).name)
+
+ render_response(template_type, new_template)
+ end
+ 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
new file mode 100644
index 00000000000..e60cb25e57b
--- /dev/null
+++ b/lib/api/v3/todos.rb
@@ -0,0 +1,30 @@
+module API
+ module V3
+ class Todos < Grape::API
+ before { authenticate! }
+
+ resource :todos do
+ desc 'Mark a todo as done' do
+ success ::API::Entities::Todo
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
+ end
+ delete ':id' do
+ todo = current_user.todos.find(params[:id])
+ TodoService.new.mark_todos_as_done([todo], current_user)
+
+ present todo.reload, with: ::API::Entities::Todo, current_user: current_user
+ end
+
+ 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)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
new file mode 100644
index 00000000000..1dfdb6a5956
--- /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 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" 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
new file mode 100644
index 00000000000..14f54731730
--- /dev/null
+++ b/lib/api/v3/users.rb
@@ -0,0 +1,149 @@
+module API
+ module V3
+ class Users < Grape::API
+ include PaginationParams
+
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
+
+ resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ desc 'Get the SSH keys of 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'
+ use :pagination
+ end
+ get ':id/keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present paginate(user.keys), with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get the emails addresses of a specified user. Available only for admins.' do
+ success ::API::Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/emails' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present user.emails, with: ::API::Entities::Email
+ end
+
+ desc 'Block a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ put ':id/block' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ if !user.ldap_blocked?
+ user.block
+ else
+ forbidden!('LDAP blocked users cannot be modified by the API')
+ end
+ end
+
+ desc 'Unblock a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ put ':id/unblock' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ if user.ldap_blocked?
+ forbidden!('LDAP blocked users cannot be unblocked by the API')
+ else
+ 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
+ desc "Get the currently authenticated user's SSH keys" do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ use :pagination
+ end
+ get "keys" do
+ present current_user.keys, with: ::API::Entities::SSHKey
+ end
+
+ desc "Get the currently authenticated user's email addresses" do
+ success ::API::Entities::Email
+ end
+ 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
+end
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
new file mode 100644
index 00000000000..0f55a14fb28
--- /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 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..77e5d54c225 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
@@ -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 cedbb289f6a..30a91647b77 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -8,6 +8,7 @@ module Backup
@name = name
@app_files_dir = File.realpath(app_files_dir)
@files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
+ @backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
@backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
end
@@ -15,14 +16,28 @@ module Backup
def dump
FileUtils.mkdir_p(Gitlab.config.backup.path)
FileUtils.rm_f(backup_tarball)
- run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+
+ if ENV['STRATEGY'] == 'copy'
+ cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path})
+ output, status = Gitlab::Popen.popen(cmd)
+
+ unless status.zero?
+ puts output
+ abort 'Backup failed'
+ end
+
+ 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])
+ end
end
def restore
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
@@ -32,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 cefbfdce3bb..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 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/pages.rb b/lib/backup/pages.rb
new file mode 100644
index 00000000000..215ded93bfe
--- /dev/null
+++ b/lib/backup/pages.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Pages < Files
+ def initialize
+ super('pages', Gitlab.config.pages.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index d746070913d..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
@@ -12,7 +12,7 @@ module Backup
path_to_project_bundle = path_to_bundle(project)
# Create namespace dir if missing
- FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
+ FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
if project.empty_repo?
$progress.puts "[SKIPPED]".color(:cyan)
@@ -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/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index 0257848b6bc..e2b57adf611 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -14,7 +14,7 @@ module Banzai
def project_from_ref(ref)
return context[:project] unless ref
- Project.find_with_namespace(ref)
+ Project.find_by_full_path(ref)
end
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index a3d495a5da0..02d5ad70fa7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -33,7 +33,12 @@ module Banzai
# Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~
+ symbol = $~[object_sym]
+ if object_class.reference_valid?(symbol)
+ yield match, symbol.to_i, $~[:project], $~[:namespace], $~
+ else
+ match
+ end
end
end
@@ -155,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)
@@ -285,7 +291,7 @@ module Banzai
end
def current_project_namespace_path
- @current_project_namespace_path ||= project.namespace.path
+ @current_project_namespace_path ||= project.namespace.full_path
end
private
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..651b55523c0 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -8,11 +8,6 @@ module Banzai
# 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',
@@ -22,9 +17,7 @@ module Banzai
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/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
new file mode 100644
index 00000000000..b2537117558
--- /dev/null
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -0,0 +1,39 @@
+require "nokogiri"
+require "asciidoctor-plantuml/plantuml"
+
+module Banzai
+ module Filter
+ # HTML that replaces all `code plantuml` tags with PlantUML img tags.
+ #
+ class PlantumlFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless doc.at('pre.plantuml') && settings.plantuml_enabled
+
+ plantuml_setup
+
+ doc.css('pre.plantuml').each do |el|
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(
+ Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {}))
+ el.replace img_tag
+ end
+
+ doc
+ end
+
+ private
+
+ def settings
+ ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ end
+
+ def plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = settings.plantuml_url
+ conf.png_enable = settings.plantuml_enabled
+ conf.svg_enable = false
+ conf.txt_enable = false
+ end
+ end
+ end
+ end
+end
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/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 1aa9355b256..849e1142841 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -75,8 +75,8 @@ module Banzai
# corresponding Namespace objects.
def namespaces
@namespaces ||=
- Namespace.where(path: usernames).each_with_object({}) do |row, hash|
- hash[row.path] = row
+ Namespace.where_full_path_in(usernames).each_with_object({}) do |row, hash|
+ hash[row.full_path] = row
end
end
@@ -122,7 +122,7 @@ module Banzai
def link_to_namespace(namespace, link_content: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace, link_content: link_content)
+ link_to_group(namespace.full_path, namespace, link_content: link_content)
else
link_to_user(namespace.path, namespace, link_content: link_content)
end
@@ -133,7 +133,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/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index ac95a79009b..b25d6f18d59 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -10,6 +10,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
+ Filter::PlantumlFilter,
Filter::SanitizationFilter,
Filter::MathFilter,
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
index 1e1b51e683e..fb2faae02bc 100644
--- a/lib/banzai/querying.rb
+++ b/lib/banzai/querying.rb
@@ -1,18 +1,64 @@
module Banzai
module Querying
+ module_function
+
# Searches a Nokogiri document using a CSS query, optionally optimizing it
# whenever possible.
#
- # document - A document/element to search.
- # query - The CSS query to use.
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ # reference_options - A hash with nodes filter options
#
- # Returns a Nokogiri::XML::NodeSet.
- def self.css(document, query)
+ # Returns an array of Nokogiri::XML::Element objects if location is specified
+ # in reference_options. Otherwise it would a Nokogiri::XML::NodeSet.
+ def css(document, query, reference_options = {})
# When using "a.foo" Nokogiri compiles this to "//a[...]" but
# "descendant::a[...]" is quite a bit faster and achieves the same result.
xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+ xpath = restrict_to_p_nodes_at_root(xpath) if filter_nodes_at_beginning?(reference_options)
+ nodes = document.xpath(xpath)
+
+ filter_nodes(nodes, reference_options)
+ end
+
+ def restrict_to_p_nodes_at_root(xpath)
+ xpath.gsub('descendant::', './p/')
+ end
+
+ def filter_nodes(nodes, reference_options)
+ if filter_nodes_at_beginning?(reference_options)
+ filter_nodes_at_beginning(nodes)
+ else
+ nodes
+ end
+ end
+
+ def filter_nodes_at_beginning?(reference_options)
+ reference_options && reference_options[:location] == :beginning
+ end
+
+ # Selects child nodes if they are present in the beginning among other siblings.
+ #
+ # nodes - A Nokogiri::XML::NodeSet.
+ #
+ # Returns an array of Nokogiri::XML::Element objects.
+ def filter_nodes_at_beginning(nodes)
+ parents_and_nodes = nodes.group_by(&:parent)
+ filtered_nodes = []
+
+ parents_and_nodes.each do |parent, nodes|
+ children = parent.children
+ nodes = nodes.to_a
+
+ children.each do |child|
+ next if child.text.blank?
+ node = nodes.shift
+ break unless node == child
+ filtered_nodes << node
+ end
+ end
- document.xpath(xpath)
+ filtered_nodes
end
end
end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index b26a41a1f3b..8e3b0c4db79 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -16,6 +16,11 @@ module Banzai
processor.process(html_documents)
end
+ def reset_memoized_values
+ @html_documents = nil
+ @texts_and_contexts = []
+ end
+
private
def html_documents
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index d8a855ec1fe..b121c37c5d0 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -33,7 +33,7 @@ module Banzai
# they have access to.
class BaseParser
class << self
- attr_accessor :reference_type
+ attr_accessor :reference_type, :reference_options
end
# Returns the attribute name containing the value for every object to be
@@ -182,9 +182,10 @@ module Banzai
# the references.
def process(documents)
type = self.class.reference_type
+ reference_options = self.class.reference_options
nodes = documents.flat_map do |document|
- Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
+ Querying.css(document, "a[data-reference-type='#{type}'].gfm", reference_options).to_a
end
gather_references(nodes)
@@ -209,7 +210,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/banzai/reference_parser/directly_addressed_user_parser.rb b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
new file mode 100644
index 00000000000..77df9bbd024
--- /dev/null
+++ b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
@@ -0,0 +1,8 @@
+module Banzai
+ module ReferenceParser
+ class DirectlyAddressedUserParser < UserParser
+ self.reference_type = :user
+ self.reference_options = { location: :beginning }
+ end
+ end
+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 c10d3616f31..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
@@ -126,7 +170,7 @@ module Ci
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
- return unless indicator == '[' and terminator == 'm'
+ return unless indicator == '[' && terminator == 'm'
close_open_tags()
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/api/triggers.rb b/lib/ci/api/triggers.rb
index 63b42113513..6e622601680 100644
--- a/lib/ci/api/triggers.rb
+++ b/lib/ci/api/triggers.rb
@@ -1,41 +1,30 @@
module Ci
module API
- # Build Trigger API
class Triggers < Grape::API
resource :projects do
- # Trigger a GitLab CI project build
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # ref (required) - The name of project's branch or tag
- # token (required) - The uniq token of trigger
- # Example Request:
- # POST /projects/:id/ref/:ref/trigger
+ desc 'Trigger a GitLab CI project build' do
+ success Entities::TriggerRequest
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a CI project'
+ requires :ref, type: String, desc: "The name of project's branch or tag"
+ requires :token, type: String, desc: 'The unique token of the trigger'
+ optional :variables, type: Hash, desc: 'Optional build variables'
+ end
post ":id/refs/:ref/trigger" do
- required_attributes! [:token]
-
- project = Project.find_by(ci_id: params[:id].to_i)
- trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ project = Project.find_by(ci_id: params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token])
not_found! unless project && trigger
unauthorized! unless trigger.project == project
- # validate variables
- variables = params[:variables]
- if variables
- unless variables.is_a?(Hash)
- render_api_error!('variables needs to be a hash', 400)
- end
-
- 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
+ # 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)
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables)
if trigger_request
present trigger_request, with: Entities::TriggerRequest
else
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/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 730b05bed97..a10b4657d7d 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -8,6 +8,6 @@ class ProjectUrlConstrainer
return false
end
- Project.find_with_namespace(full_path).present?
+ Project.find_by_full_path(full_path).present?
end
end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2edddb84fc3..7f5f6d9ddb6 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/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 0618107e2c3..d575367d81a 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -36,6 +36,9 @@ module Gitlab
html = Banzai.post_process(html, context)
+ filter = Banzai::Filter::SanitizationFilter.new(html)
+ html = filter.call.to_s
+
html.html_safe
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/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
index 548f85b78bb..4a049ef758d 100644
--- a/lib/gitlab/badge/metadata.rb
+++ b/lib/gitlab/badge/metadata.rb
@@ -20,6 +20,10 @@ module Gitlab
"[![#{title}](#{image_url})](#{link_url})"
end
+ def to_asciidoc
+ "image:#{image_url}[link=\"#{link_url}\",title=\"#{title}\"]"
+ end
+
def title
raise NotImplementedError
end
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/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb
deleted file mode 100644
index dfb1c8f6616..00000000000
--- a/lib/gitlab/chat_commands/presenters/issuable.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module Gitlab
- module ChatCommands
- module Presenters
- module Issuable
- def color(issuable)
- issuable.open? ? '#38ae67' : '#d22852'
- end
-
- def status_text(issuable)
- issuable.open? ? 'Open' : 'Closed'
- end
-
- def project
- @resource.project
- end
-
- def author
- @resource.author
- end
-
- def fields
- [
- {
- title: "Assignee",
- value: @resource.assignee ? @resource.assignee.name : "_None_",
- short: true
- },
- {
- title: "Milestone",
- value: @resource.milestone ? @resource.milestone.title : "_None_",
- short: true
- },
- {
- title: "Labels",
- value: @resource.labels.any? ? @resource.label_names : "_None_",
- short: true
- }
- ]
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
new file mode 100644
index 00000000000..054f7f4be0c
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ module IssueBase
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignee ? @resource.assignee.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
index a1a3add56c9..3674ba25641 100644
--- a/lib/gitlab/chat_commands/presenters/issue_new.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -2,7 +2,7 @@ module Gitlab
module ChatCommands
module Presenters
class IssueNew < Presenters::Base
- include Presenters::Issuable
+ include Presenters::IssueBase
def present
in_channel_response(new_issue)
@@ -10,7 +10,7 @@ module Gitlab
private
- def new_issue
+ def new_issue
{
attachments: [
{
@@ -38,7 +38,7 @@ module Gitlab
end
def project_link
- "[#{project.name_with_namespace}](#{projects_url(project)})"
+ "[#{project.name_with_namespace}](#{project.web_url})"
end
def author_profile_link
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
index 3478359b91d..73788cf9662 100644
--- a/lib/gitlab/chat_commands/presenters/issue_search.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -2,7 +2,7 @@ module Gitlab
module ChatCommands
module Presenters
class IssueSearch < Presenters::Base
- include Presenters::Issuable
+ include Presenters::IssueBase
def present
text = if @resource.count >= 5
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
index fe5847ccd15..bd784ad241e 100644
--- a/lib/gitlab/chat_commands/presenters/issue_show.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -2,7 +2,7 @@ module Gitlab
module ChatCommands
module Presenters
class IssueShow < Presenters::Base
- include Presenters::Issuable
+ include Presenters::IssueBase
def present
if @resource.confidential?
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/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 833ae4a0ff3..e05aca9881b 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -58,7 +58,7 @@ module Gitlab
def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
- @entries[symbol].specified? if @entries[symbol]
+ @entries[symbol]&.specified?
end
define_method("#{symbol}_value") do
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/global.rb b/lib/gitlab/ci/config/entry/global.rb
index ede97cc0504..a4ec8f0ff2f 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -33,11 +33,8 @@ module Gitlab
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.'
- entry :coverage, Entry::Coverage,
- description: 'Coverage configuration for this pipeline.'
-
helpers :before_script, :image, :services, :after_script,
- :variables, :stages, :types, :cache, :coverage, :jobs
+ :variables, :stages, :types, :cache, :jobs
def compose!(_deps = nil)
super(self) do
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..d3524c338ee 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -1,25 +1,15 @@
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
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/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 7e3d5647b39..15992b77680 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -22,8 +22,10 @@ module Gitlab
having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
mr_events = event_counts(date_from, :merge_requests).
having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
+ note_events = event_counts(date_from, :merge_requests).
+ having(action: [Event::COMMENTED], target_type: "Note")
- union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
+ union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events])
events = Event.find_by_sql(union.to_sql).map(&:attributes)
@activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
@@ -38,7 +40,7 @@ module Gitlab
# Use visible_to_user? instead of the complicated logic in activity_dates
# because we're only viewing the events for a single day.
- events.select {|event| event.visible_to_user?(current_user) }
+ events.select { |event| event.visible_to_user?(current_user) }
end
def starting_year
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/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 0d8791d396b..ab115afcaa5 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -5,6 +5,8 @@ module Gitlab
attr_reader :projections, :query, :stage, :order
+ MAX_EVENTS = 50
+
def initialize(project:, stage:, options:)
@project = project
@stage = stage
@@ -38,7 +40,7 @@ module Gitlab
def events_query
diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
- base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc).take(MAX_EVENTS)
end
def default_order
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
index d1bc2055ba8..1e52b6614a1 100644
--- a/lib/gitlab/cycle_analytics/code_stage.rb
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:code
end
+ def legend
+ "Related Merge Requests"
+ end
+
def description
"Time until first merge request"
end
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
index d2068fbc38f..213994988a5 100644
--- a/lib/gitlab/cycle_analytics/issue_stage.rb
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:issue
end
+ def legend
+ "Related Issues"
+ end
+
def description
"Time before an issue gets scheduled"
end
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
index 3b4dfc6a30e..45d51d30ccc 100644
--- a/lib/gitlab/cycle_analytics/plan_stage.rb
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:plan
end
+ def legend
+ "Related Commits"
+ end
+
def description
"Time before an issue starts implementation"
end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
index 2a6bcc80116..9f387a02945 100644
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -15,6 +15,10 @@ module Gitlab
:production
end
+ def legend
+ "Related Issues"
+ end
+
def description
"From issue creation until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
index fbaa3010d81..4744be834de 100644
--- a/lib/gitlab/cycle_analytics/review_stage.rb
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:review
end
+ def legend
+ "Relative Merged Requests"
+ end
+
def description
"Time between merge request creation and merge/close"
end
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
index 945909a4d62..3cdbe04fbaf 100644
--- a/lib/gitlab/cycle_analytics/staging_stage.rb
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:staging
end
+ def legend
+ "Relative Deployed Builds"
+ end
+
def description
"From merge request merge until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 0079d56e0e4..e96943833bc 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:test
end
+ def legend
+ "Relative Builds Trigger by Commits"
+ end
+
def description
"Total test time for all commits/merges"
end
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 6548e6475c6..f78106f5b10 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -8,6 +8,8 @@ module Gitlab
commit = build.pipeline
user = build.user
+ author_url = build_author_url(build.commit, commit)
+
data = {
object_kind: 'build',
@@ -43,6 +45,7 @@ module Gitlab
message: commit.git_commit_message,
author_name: commit.git_author_name,
author_email: commit.git_author_email,
+ author_url: author_url,
status: commit.status,
duration: commit.duration,
started_at: commit.started_at,
@@ -62,6 +65,13 @@ module Gitlab
data
end
+
+ private
+
+ def build_author_url(commit, pipeline)
+ author = commit.try(:author)
+ author ? Gitlab::Routing.url_helpers.user_url(author) : "mailto:#{pipeline.git_author_email}"
+ end
end
end
end
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 55b8f888d53..f3f417c1a63 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -6,7 +6,7 @@ module Gitlab
MAX_INT_VALUE = 2147483647
def self.adapter_name
- connection.adapter_name
+ ActiveRecord::Base.configurations[Rails.env]['adapter']
end
def self.mysql?
@@ -24,7 +24,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
@@ -35,8 +35,22 @@ module Gitlab
order
end
+ def self.nulls_first_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if postgresql?
+ order << ' NULLS FIRST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'DESC'
+ end
+
+ order
+ end
+
def self.random
- Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ postgresql? ? "RANDOM()" : "RAND()"
end
def true_value
@@ -55,6 +69,36 @@ module Gitlab
end
end
+ def self.with_connection_pool(pool_size)
+ pool = create_connection_pool(pool_size)
+
+ begin
+ yield(pool)
+ ensure
+ pool.disconnect!
+ end
+ end
+
+ # 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 =
+ ActiveRecord::
+ ConnectionAdapters::
+ ConnectionSpecification::Resolver.new(config).spec(env.to_sym)
+
+ ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
+ end
+
def self.connection
ActiveRecord::Base.connection
end
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/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 0bd6e148ba8..fc445ab9483 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -26,11 +26,68 @@ module Gitlab
add_index(table_name, column_name, options)
end
+ # Adds a foreign key with only minimal locking on the tables involved.
+ #
+ # This method only requires minimal locking when using PostgreSQL. When
+ # using MySQL this method will use Rails' default `add_foreign_key`.
+ #
+ # source - The source table containing the foreign key.
+ # target - The target table the key points to.
+ # column - The name of the column to create the foreign key on.
+ # on_delete - The action to perform when associated data is removed,
+ # defaults to "CASCADE".
+ def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade)
+ # Transactions would result in ALTER TABLE locks being held for the
+ # duration of the transaction, defeating the purpose of this method.
+ if transaction_open?
+ raise 'add_concurrent_foreign_key can not be run inside a transaction'
+ end
+
+ # While MySQL does allow disabling of foreign keys it has no equivalent
+ # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
+ # back to the normal foreign key procedure.
+ if Database.mysql?
+ return add_foreign_key(source, target,
+ column: column,
+ on_delete: on_delete)
+ end
+
+ disable_statement_timeout
+
+ key_name = concurrent_foreign_key_name(source, column)
+
+ # Using NOT VALID allows us to create a key without immediately
+ # validating it. This means we keep the ALTER TABLE lock only for a
+ # short period of time. The key _is_ enforced for any newly created
+ # data.
+ execute <<-EOF.strip_heredoc
+ ALTER TABLE #{source}
+ ADD CONSTRAINT #{key_name}
+ FOREIGN KEY (#{column})
+ REFERENCES #{target} (id)
+ ON DELETE #{on_delete} NOT VALID;
+ EOF
+
+ # Validate the existing constraint. This can potentially take a very
+ # long time to complete, but fortunately does not lock the source table
+ # while running.
+ execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
+ end
+
+ # Returns the name for a concurrent foreign key.
+ #
+ # PostgreSQL constraint names have a limit of 63 bytes. The logic used
+ # here is based on Rails' foreign_key_name() method, which unfortunately
+ # is private so we can't rely on it directly.
+ def concurrent_foreign_key_name(table, column)
+ "fk_#{Digest::SHA256.hexdigest("#{table}_#{column}_fk").first(10)}"
+ end
+
# Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only)
def disable_statement_timeout
- ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql?
+ execute('SET statement_timeout TO 0') if Database.postgresql?
end
# Updates the value of a column in batches.
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/parser.rb b/lib/gitlab/diff/parser.rb
index 59a2367b65d..8f844224a7a 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -20,7 +20,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
@@ -45,7 +45,7 @@ module Gitlab
line_new += 1
when "-"
line_old += 1
- when "\\"
+ when "\\" # rubocop:disable Lint/EmptyWhen
# No increment
else
line_new += 1
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/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 127fae159d5..b8ec9138c10 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -34,7 +34,7 @@ module Gitlab
end
def project
- @project ||= Project.find_with_namespace(project_path)
+ @project ||= Project.find_by_full_path(project_path)
end
private
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 0e3b65fceb4..6c69cd9e6a9 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def diffs_count
- diffs.size if diffs
+ diffs&.size
end
def compare
@@ -58,7 +58,7 @@ module Gitlab
end
def compare_timeout
- diffs.overflow? if diffs
+ diffs&.overflow?
end
def reverse_compare?
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index a40c44eb1bc..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)
@@ -35,6 +35,8 @@ module Gitlab
handler.execute
end
+ private
+
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
@@ -54,7 +56,24 @@ module Gitlab
end
def key_from_additional_headers(mail)
- Array(mail.references).find do |mail_id|
+ references = ensure_references_array(mail.references)
+
+ find_key_from_references(references)
+ end
+
+ def ensure_references_array(references)
+ case references
+ when Array
+ references
+ when String
+ # Handle emails from clients which append with commas,
+ # example clients are Microsoft exchange and iOS app
+ Gitlab::IncomingEmail.scan_fallback_references(references)
+ end
+ end
+
+ def find_key_from_references(references)
+ references.find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key
end
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..35871fd1b7b 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,42 @@ 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, image: false, sprite: false, force_fallback: false)
+ emoji_name = emojis_aliases[name] || name
+ emoji_info = emojis[emoji_name]
+ emoji_fallback_image_source = ActionController::Base.helpers.url_to_image("emoji/#{emoji_info['name']}.png")
+ emoji_fallback_sprite_class = "emoji-#{emoji_name}"
+
+ data = {
+ name: emoji_name,
+ unicode_version: emoji_unicode_version(emoji_name)
+ }
+ data[:fallback_src] = emoji_fallback_image_source if image
+ data[:fallback_sprite_class] = emoji_fallback_sprite_class if sprite
+ ActionController::Base.helpers.content_tag 'gl-emoji',
+ class: ("emoji-icon #{emoji_fallback_sprite_class}" if force_fallback && sprite),
+ data: data do
+ if force_fallback && !sprite
+ emoji_image_tag(emoji_name, emoji_fallback_image_source)
+ else
+ emoji_info['moji']
+ end
+ end
+ 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.rb b/lib/gitlab/git.rb
index 3cd515e4a3a..d3df3f1bca1 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def ref_name(ref)
- ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ ref.sub(/\Arefs\/(tags|heads)\//, '')
end
def branch_name(ref)
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/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb
index e98de57fc22..d7975f88aaa 100644
--- a/lib/gitlab/git/blob_snippet.rb
+++ b/lib/gitlab/git/blob_snippet.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def data
- lines.join("\n") if lines
+ lines&.join("\n")
end
def name
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..2a017c93f57 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
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..228ef7bb7a9 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.parameterize}-#{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_post_receive.rb b/lib/gitlab/git_post_receive.rb
index d32bdd86427..6babea144c7 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -30,11 +30,11 @@ module Gitlab
def retrieve_project_and_type
@type = :project
- @project = Project.find_with_namespace(@repo_path)
+ @project = Project.find_by_full_path(@repo_path)
if @repo_path.end_with?('.wiki') && !@project
@type = :wiki
- @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, ''))
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
new file mode 100644
index 00000000000..b981a629fb0
--- /dev/null
+++ b/lib/gitlab/gitaly_client.rb
@@ -0,0 +1,29 @@
+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
+ 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/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 95dba9a327b..8c80791e7c9 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -1,11 +1,12 @@
module Gitlab
module GithubImport
class BaseFormatter
- attr_reader :formatter, :project, :raw_data
+ attr_reader :client, :formatter, :project, :raw_data
- def initialize(project, raw_data)
+ def initialize(project, raw_data, client = nil)
@project = project
@raw_data = raw_data
+ @client = client
@formatter = Gitlab::ImportFormatter.new
end
@@ -18,19 +19,6 @@ module Gitlab
def url
raw_data.url || ''
end
-
- private
-
- def gitlab_user_id(github_id)
- User.joins(:identities).
- find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
- try(:id)
- end
-
- def gitlab_author_id
- return @gitlab_author_id if defined?(@gitlab_author_id)
- @gitlab_author_id = gitlab_user_id(raw_data.user.id)
- 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/client.rb b/lib/gitlab/github_import/client.rb
index ba869faa92e..7dbeec5b010 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -10,6 +10,7 @@ module Gitlab
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
@api_version = api_version
+ @users = {}
if access_token
::Octokit.auto_paginate = false
@@ -64,6 +65,13 @@ module Gitlab
api.respond_to?(method) || super
end
+ def user(login)
+ return nil unless login.present?
+ return @users[login] if @users.key?(login)
+
+ @users[login] = api.user(login)
+ end
+
private
def api_endpoint
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2bddcde2b7c..e21922070c1 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class CommentFormatter < BaseFormatter
+ attr_writer :author_id
+
def attributes
{
project: project,
@@ -17,11 +19,11 @@ module Gitlab
private
def author
- raw_data.user.login
+ @author ||= UserFormatter.new(client, raw_data.user)
end
def author_id
- gitlab_author_id || project.creator_id
+ author.gitlab_id || project.creator_id
end
def body
@@ -52,10 +54,10 @@ module Gitlab
end
def note
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index ec1318ab33c..eea4a91f17d 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -110,12 +110,12 @@ module Gitlab
def import_issues
fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw)
+ gh_issue = IssueFormatter.new(project, raw, client)
begin
issuable =
if gh_issue.pull_request?
- MergeRequest.find_by_iid(gh_issue.number)
+ MergeRequest.find_by(target_project_id: project.id, iid: gh_issue.number)
else
gh_issue.create!
end
@@ -131,7 +131,8 @@ module Gitlab
def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
- gh_pull_request = PullRequestFormatter.new(project, raw)
+ gh_pull_request = PullRequestFormatter.new(project, raw, client)
+
next unless gh_pull_request.valid?
begin
@@ -170,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
@@ -209,11 +212,17 @@ module Gitlab
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
+ comment = CommentFormatter.new(project, raw, client)
+
# GH does not return info about comment's parent, so we guess it by checking its URL!
*_, parent, iid = URI(raw.html_url).path.split('/')
- issuable_class = parent == 'issues' ? Issue : MergeRequest
- issuable = issuable_class.find_by_iid(iid)
+
+ issuable = if parent == 'issues'
+ Issue.find_by(project_id: project.id, iid: iid)
+ else
+ MergeRequest.find_by(target_project_id: project.id, iid: iid)
+ end
+
next unless issuable
issuable.notes.create!(comment.attributes)
@@ -278,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 256f360efc7..27b171d6ddb 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -1,13 +1,13 @@
module Gitlab
module GithubImport
class IssuableFormatter < BaseFormatter
+ attr_writer :assignee_id, :author_id
+
def project_association
raise NotImplementedError
end
- def number
- raw_data.number
- end
+ delegate :number, to: :raw_data
def find_condition
{ iid: number }
@@ -23,18 +23,24 @@ module Gitlab
raw_data.assignee.present?
end
- def assignee_id
+ def author
+ @author ||= UserFormatter.new(client, raw_data.user)
+ end
+
+ def author_id
+ @author_id ||= author.gitlab_id || project.creator_id
+ end
+
+ def assignee
if assigned?
- gitlab_user_id(raw_data.assignee.id)
+ @assignee ||= UserFormatter.new(client, raw_data.assignee)
end
end
- def author
- raw_data.user.login
- end
+ def assignee_id
+ return @assignee_id if defined?(@assignee_id)
- def author_id
- gitlab_author_id || project.creator_id
+ @assignee_id = assignee.try(:gitlab_id)
end
def body
@@ -42,10 +48,10 @@ module Gitlab
end
def description
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
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/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb
new file mode 100644
index 00000000000..04c2964da20
--- /dev/null
+++ b/lib/gitlab/github_import/user_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class UserFormatter
+ attr_reader :client, :raw
+
+ delegate :id, :login, to: :raw, allow_nil: true
+
+ def initialize(client, raw)
+ @client = client
+ @raw = raw
+ end
+
+ def gitlab_id
+ return @gitlab_id if defined?(@gitlab_id)
+
+ @gitlab_id = find_by_external_uid || find_by_email
+ end
+
+ private
+
+ def email
+ @email ||= client.user(raw.login).try(:email)
+ end
+
+ def find_by_email
+ return nil unless email
+
+ User.find_by_any_email(email)
+ .try(:id)
+ end
+
+ def find_by_external_uid
+ return nil unless id
+
+ identities = ::Identity.arel_table
+
+ User.select(:id)
+ .joins(:identities).where(identities[:provider].eq(:github)
+ .and(identities[:extern_uid].eq(id)))
+ .first
+ .try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index b8a5ac907a4..6c275a8d5de 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 = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen"
+ 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/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 1f4edc36928..b02b9737493 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -310,7 +310,7 @@ module Gitlab
if name == project.import_source
"##{id}"
else
- "#{project.namespace.path}/#{name}##{id}"
+ "#{project.namespace.full_path}/#{name}##{id}"
end
text = "~~#{text}~~" if deleted
text
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index d679edec36b..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.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/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index f00c7460e82..90942774a2e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -15,14 +15,6 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end
- def git_unbundle(repo_path:, bundle_path:)
- execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
- end
-
- def git_restore_hooks
- execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- end
-
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
@@ -56,10 +48,6 @@ module Gitlab
FileUtils.copy_entry(source, destination)
true
end
-
- def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
- end
end
end
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/importer.rb b/lib/gitlab/import_export/importer.rb
index e9ee47fc090..063ce74ecad 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -56,7 +56,7 @@ module Gitlab
end
def path_with_namespace
- File.join(@project.namespace.path, @project.path)
+ File.join(@project.namespace.full_path, @project.path)
end
def repo_path
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index a09577ae48d..8b8e48aac76 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -32,6 +32,10 @@ module Gitlab
@user.id
end
+ def include?(old_author_id)
+ map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
+ end
+
private
def missing_keys_tracking_hash
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2fbf437ec26..3473b466936 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -5,8 +5,9 @@ module Gitlab
attr_reader :full_path
- def initialize(project:, shared:)
+ def initialize(project:, current_user:, shared:)
@project = project
+ @current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
@@ -24,7 +25,35 @@ module Gitlab
private
def project_json_tree
- @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ project_json['project_members'] += group_members_json
+
+ project_json.to_json
+ end
+
+ def project_json
+ @project_json ||= @project.as_json(reader.project_tree)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
+ end
+
+ def group_members_json
+ group_members.as_json(reader.group_members_tree).each do |group_member|
+ group_member['source_type'] = 'Project' # Make group members project members of the future import
+ end
+ end
+
+ def group_members
+ return [] unless @current_user.can?(:admin_group, @project.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.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/reader.rb b/lib/gitlab/import_export/reader.rb
index 5021a1a14ce..a1e7159fe42 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -21,6 +21,10 @@ module Gitlab
false
end
+ def group_members_tree
+ @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
+ end
+
private
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 0319d7707a8..fae792237d9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -89,7 +89,7 @@ module Gitlab
end
def has_author?(old_author_id)
- admin_user? && @members_mapper.map.keys.include?(old_author_id)
+ admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 48a9a6fa5e2..c824d3ea9fc 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
+ include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:)
@project = project
@@ -12,29 +13,11 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- mkdir_p(path_to_repo)
-
- git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
+ gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle)
rescue => e
@shared.error(e)
false
end
-
- private
-
- def path_to_repo
- @project.repository.path_to_repo
- end
-
- def repo_restore_hooks
- return true if wiki?
-
- git_restore_hooks
- end
-
- def wiki?
- @project.class.name == 'ProjectWiki'
- end
end
end
end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index b91012d6405..c9122a23568 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -4,8 +4,6 @@ module Gitlab
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
- FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
-
def enabled?
config.enabled && config.address
end
@@ -37,10 +35,14 @@ module Gitlab
end
def key_from_fallback_message_id(mail_id)
- match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX)
- return unless match
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
- match[1]
+ mail_id[message_id_regexp, 1]
+ end
+
+ def scan_fallback_references(references)
+ # It's looking for each <...>
+ references.scan(/(?!<)[^<>]+(?=>)/)
end
def config
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 288771c1c12..3a7af363548 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -43,10 +43,10 @@ module Gitlab
end
end
- def add_terminal_auth(terminal, token, ca_pem = nil)
+ def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
- terminal
end
def container_exec_url(api_url, namespace, pod_name, container_name)
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/metrics.rb b/lib/gitlab/metrics.rb
index 3d1ba33ec68..857e0abf710 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -112,7 +112,7 @@ module Gitlab
def self.tag_transaction(name, value)
trans = current_transaction
- trans.add_tag(name, value) if trans
+ trans&.add_tag(name, value)
end
# Sets the action of the current transaction (if any)
@@ -121,7 +121,7 @@ module Gitlab
def self.action=(action)
trans = current_transaction
- trans.action = action if trans
+ trans&.action = action
end
# Tracks an event.
@@ -130,7 +130,7 @@ module Gitlab
def self.add_event(*args)
trans = current_transaction
- trans.add_event(*args) if trans
+ trans&.add_event(*args)
end
# Returns the prefix to use for the name of a series.
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/system.rb b/lib/gitlab/metrics/system.rb
index 287b7a83547..3aaebb3e9c3 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -11,7 +11,7 @@ module Gitlab
mem = 0
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
- if match and match[1]
+ if match && match[1]
mem = match[1].to_f * 1024
end
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
new file mode 100644
index 00000000000..6105d165810
--- /dev/null
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -0,0 +1,24 @@
+# This Rack middleware is intended to proxy the webpack assets directory to the
+# webpack-dev-server. It is only intended for use in development.
+
+module Gitlab
+ module Middleware
+ class WebpackProxy < Rack::Proxy
+ def initialize(app = nil, opts = {})
+ @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, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
+ end
+
+ def perform_request(env)
+ if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ super(env)
+ else
+ @app.call(env)
+ end
+ end
+ 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/other_markup.rb b/lib/gitlab/other_markup.rb
index 4e2f8ed5587..e67acf28c94 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -17,6 +17,9 @@ module Gitlab
html = Banzai.post_process(html, context)
+ filter = Banzai::Filter::SanitizationFilter.new(html)
+ html = filter.call.to_s
+
html.html_safe
end
end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
new file mode 100644
index 00000000000..fb215f27cbd
--- /dev/null
+++ b/lib/gitlab/pages_transfer.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PagesTransfer < ProjectTransfer
+ def root_dir
+ Gitlab.config.pages.path
+ end
+ end
+end
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
new file mode 100644
index 00000000000..1bba0b78e2f
--- /dev/null
+++ b/lib/gitlab/project_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class ProjectTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ def root_dir
+ raise NotImplementedError
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+ 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/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 70e7f25d518..4bc76ea033f 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -10,5 +10,9 @@ module Gitlab
true
end
end
+
+ def self.enabled?
+ current_application_settings.recaptcha_enabled
+ end
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9384102acec..62dbd429156 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -4,24 +4,15 @@ require 'active_support/core_ext/hash/keys'
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'
+ 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
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
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) }
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 11c0b01f0dc..7668ecacc4b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,13 +1,12 @@
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)
+ 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)
@project = project
@current_user = current_user
-
@references = {}
super()
@@ -21,6 +20,11 @@ module Gitlab
super(type, project, current_user)
end
+ def reset_memoized_values
+ @references = {}
+ super()
+ end
+
REFERABLES.each do |type|
define_method("#{type}s") do
@references[type] ||= references(type)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index a3fa7c1331a..5e5f5ff1589 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,13 +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
+ 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/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
index 786e1d49f5e..ef42b0557e0 100644
--- a/lib/gitlab/request_profiler/middleware.rb
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -1,5 +1,4 @@
require 'ruby-prof'
-require_dependency 'gitlab/request_profiler'
module Gitlab
module RequestProfiler
@@ -20,7 +19,7 @@ module Gitlab
header_token = env['HTTP_X_PROFILE_TOKEN']
return unless header_token.present?
- profile_token = RequestProfiler.profile_token
+ profile_token = Gitlab::RequestProfiler.profile_token
return unless profile_token.present?
header_token == profile_token
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
new file mode 100644
index 00000000000..36791fae60f
--- /dev/null
+++ b/lib/gitlab/route_map.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class RouteMap
+ FormatError = Class.new(StandardError)
+
+ def initialize(data)
+ begin
+ entries = YAML.safe_load(data)
+ rescue
+ raise FormatError, 'Route map is not valid YAML'
+ end
+
+ raise FormatError, 'Route map is not an array' unless entries.is_a?(Array)
+
+ @map = entries.map { |entry| parse_entry(entry) }
+ end
+
+ def public_path_for_source_path(path)
+ mapping = @map.find { |mapping| mapping[:source] === path }
+ return unless mapping
+
+ path.sub(mapping[:source], mapping[:public])
+ end
+
+ private
+
+ def parse_entry(entry)
+ raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
+ raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+
+ source_pattern = entry['source']
+ public_path = entry['public']
+
+ if source_pattern.start_with?('/') && source_pattern.end_with?('/')
+ source_pattern = source_pattern[1...-1].gsub('\/', '/')
+
+ begin
+ source_pattern = /\A#{source_pattern}\z/
+ rescue RegexpError => e
+ raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
+ end
+ end
+
+ {
+ source: source_pattern,
+ public: public_path
+ }
+ end
+ end
+end
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/serialize/ci/variables.rb b/lib/gitlab/serialize/ci/variables.rb
deleted file mode 100644
index 3a9443bfcd9..00000000000
--- a/lib/gitlab/serialize/ci/variables.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module Serialize
- module Ci
- # This serializer could make sure our YAML variables' keys and values
- # are always strings. This is more for legacy build data because
- # from now on we convert them into strings before saving to database.
- module Variables
- extend self
-
- def load(string)
- return unless string
-
- object = YAML.safe_load(string, [Symbol])
-
- object.map do |variable|
- variable[:key] = variable[:key].to_s
- variable
- end
- end
-
- def dump(object)
- YAML.dump(object)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/serializer/ci/variables.rb b/lib/gitlab/serializer/ci/variables.rb
new file mode 100644
index 00000000000..c059c454eac
--- /dev/null
+++ b/lib/gitlab/serializer/ci/variables.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Serializer
+ module Ci
+ # This serializer could make sure our YAML variables' keys and values
+ # are always strings. This is more for legacy build data because
+ # from now on we convert them into strings before saving to database.
+ module Variables
+ extend self
+
+ def load(string)
+ return unless string
+
+ object = YAML.safe_load(string, [Symbol])
+
+ object.map do |variable|
+ variable[:key] = variable[:key].to_s
+ variable
+ end
+ end
+
+ def dump(object)
+ YAML.dump(object)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
new file mode 100644
index 00000000000..9c92b83dddc
--- /dev/null
+++ b/lib/gitlab/serializer/pagination.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Serializer
+ class Pagination
+ InvalidResourceError = Class.new(StandardError)
+ include ::API::Helpers::Pagination
+
+ def initialize(request, response)
+ @request = request
+ @response = response
+ end
+
+ def paginate(resource)
+ if resource.respond_to?(:page)
+ super(resource)
+ else
+ raise InvalidResourceError
+ end
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+
+ attr_reader :request
+
+ def params
+ @request.query_parameters
+ end
+
+ def header(header, value)
+ @response.headers[header] = value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 82e194c1af1..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)
@@ -80,8 +80,10 @@ module Gitlab
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(storage, name, url)
- output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, '900'])
+ # 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'])
raise Error, output unless status.zero?
true
end
@@ -143,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
@@ -172,7 +174,7 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab")
#
def add_namespace(storage, name)
- FileUtils.mkdir(full_path(storage, name), mode: 0770) unless exists?(storage, name)
+ FileUtils.mkdir_p(full_path(storage, name), mode: 0770) unless exists?(storage, name)
end
# Remove directory from repositories storage
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/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb
index 779a9998b22..d47609f490d 100644
--- a/lib/gitlab/sidekiq_status/client_middleware.rb
+++ b/lib/gitlab/sidekiq_status/client_middleware.rb
@@ -2,7 +2,7 @@ module Gitlab
module SidekiqStatus
class ClientMiddleware
def call(_, job, _, _)
- SidekiqStatus.set(job['jid'])
+ Gitlab::SidekiqStatus.set(job['jid'])
yield
end
end
diff --git a/lib/gitlab/sidekiq_status/server_middleware.rb b/lib/gitlab/sidekiq_status/server_middleware.rb
index 31dfa46ff9d..ceab10b8301 100644
--- a/lib/gitlab/sidekiq_status/server_middleware.rb
+++ b/lib/gitlab/sidekiq_status/server_middleware.rb
@@ -4,7 +4,7 @@ module Gitlab
def call(worker, job, queue)
ret = yield
- SidekiqStatus.unset(job['jid'])
+ Gitlab::SidekiqStatus.unset(job['jid'])
ret
end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
index a672e5e4855..6dbb467d70d 100644
--- a/lib/gitlab/slash_commands/extractor.rb
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -103,7 +103,7 @@ module Gitlab
(?<cmd>#{Regexp.union(names)})
(?:
[ ]
- (?<arg>[^\/\n]*)
+ (?<arg>[^\n]*)
)?
(?:\n|$)
)
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 9e01f02029c..b85f70e450e 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -31,11 +31,11 @@ module Gitlab
private
def snippet_titles
- limit_snippets.search(query).order('updated_at DESC')
+ limit_snippets.search(query).order('updated_at DESC').includes(:author)
end
def snippet_blobs
- limit_snippets.search_code(query).order('updated_at DESC')
+ limit_snippets.search_code(query).order('updated_at DESC').includes(:author)
end
def default_scope
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/themes.rb b/lib/gitlab/themes.rb
deleted file mode 100644
index 19ab76ae80f..00000000000
--- a/lib/gitlab/themes.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module Gitlab
- # Module containing GitLab's application theme definitions and helper methods
- # for accessing them.
- module Themes
- extend self
-
- # Theme ID used when no `default_theme` configuration setting is provided.
- APPLICATION_DEFAULT = 2
-
- # Struct class representing a single Theme
- Theme = Struct.new(:id, :name, :css_class)
-
- # All available Themes
- THEMES = [
- Theme.new(1, 'Graphite', 'ui_graphite'),
- Theme.new(2, 'Charcoal', 'ui_charcoal'),
- Theme.new(3, 'Green', 'ui_green'),
- Theme.new(4, 'Black', 'ui_black'),
- Theme.new(5, 'Violet', 'ui_violet'),
- Theme.new(6, 'Blue', 'ui_blue')
- ].freeze
-
- # Convenience method to get a space-separated String of all the theme
- # classes that might be applied to the `body` element
- #
- # Returns a String
- def body_classes
- THEMES.collect(&:css_class).uniq.join(' ')
- end
-
- # Get a Theme by its ID
- #
- # If the ID is invalid, returns the default Theme.
- #
- # id - Integer ID
- #
- # Returns a Theme
- def by_id(id)
- THEMES.detect { |t| t.id == id } || default
- end
-
- # Returns the number of defined Themes
- def count
- THEMES.size
- end
-
- # Get the default Theme
- #
- # Returns a Theme
- def default
- by_id(default_id)
- end
-
- # Iterate through each Theme
- #
- # Yields the Theme object
- def each(&block)
- THEMES.each(&block)
- end
-
- # Get the Theme for the specified user, or the default
- #
- # user - User record
- #
- # Returns a Theme
- def for_user(user)
- if user
- by_id(user.theme_id)
- else
- default
- end
- end
-
- private
-
- def default_id
- id = Gitlab.config.gitlab.default_theme.to_i
-
- # Prevent an invalid configuration setting from causing an infinite loop
- if id < THEMES.first.id || id > THEMES.last.id
- APPLICATION_DEFAULT
- else
- id
- end
- end
- 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 e78d0c34a02..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,15 +59,18 @@ 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 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
def env
- { 'RAILS_ENV' => 'production' }
+ {
+ 'RAILS_ENV' => 'production',
+ 'NODE_ENV' => 'production'
+ }
end
def upgrade
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index be8fcc7b2d2..81701831a6a 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,33 +1,5 @@
module Gitlab
- class UploadsTransfer
- def move_project(project_path, namespace_path_was, namespace_path)
- new_namespace_folder = File.join(root_dir, namespace_path)
- FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
- from = File.join(root_dir, namespace_path_was, project_path)
- to = File.join(root_dir, namespace_path, project_path)
- move(from, to, "")
- end
-
- def rename_project(path_was, path, namespace_path)
- base_dir = File.join(root_dir, namespace_path)
- move(path_was, path, base_dir)
- end
-
- def rename_namespace(path_was, path)
- move(path_was, path)
- end
-
- private
-
- def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
- from = File.join(base_dir, path_was)
- to = File.join(base_dir, path)
- FileUtils.mv(from, to)
- rescue Errno::ENOENT
- false
- end
-
+ class UploadsTransfer < ProjectTransfer
def root_dir
File.join(Rails.root, "public", "uploads")
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 19dad699edf..c81dc7e30d0 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
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 c7953af29dd..2248763c106 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -13,7 +13,19 @@ module Gitlab
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
- scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
+ scope :public_to_user, -> (user) do
+ if user
+ if user.admin?
+ all
+ elsif !user.external?
+ public_and_internal_only
+ else
+ public_only
+ end
+ else
+ public_only
+ end
+ end
end
PRIVATE = 0 unless const_defined?(:PRIVATE)
@@ -21,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
@@ -33,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
@@ -72,18 +94,39 @@ module Gitlab
level_name
end
+
+ def level_value(level)
+ return string_options[level] if level.is_a? String
+ level
+ 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 a3b502ffd6a..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
@@ -107,7 +108,8 @@ module Gitlab
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
- 'Header' => terminal[:headers]
+ 'Header' => terminal[:headers],
+ 'MaxSessionTime' => terminal[:max_session_time],
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
@@ -153,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/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
new file mode 100644
index 00000000000..7d5700b7f6d
--- /dev/null
+++ b/lib/rouge/lexers/plantuml.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Plantuml < Lexer
+ title "A passthrough lexer used for PlantUML input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'plantuml'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 31b00ff128a..5fd7f0f98bd 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+gitlab_pages_enabled=false
+gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd)
+gitlab_pages_pid_path="$pid_path/gitlab-pages.pid"
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -89,13 +94,20 @@ check_pids(){
mpid=0
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ -f "$gitlab_pages_pid_path" ]; then
+ gppid=$(cat "$gitlab_pages_pid_path")
+ else
+ gppid=0
+ fi
+ fi
}
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; } || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -144,7 +156,15 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ $gppid -ne 0 ]; then
+ kill -0 "$gppid" 2>/dev/null
+ gitlab_pages_status="$?"
+ else
+ gitlab_pages_status="-1"
+ fi
+ fi
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -186,12 +206,19 @@ check_stale_pids(){
exit 1
fi
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran."
+ if ! rm "$gitlab_pages_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
}
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -213,6 +240,9 @@ start_gitlab() {
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Starting GitLab Pages"
+ fi
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
@@ -252,6 +282,16 @@ start_gitlab() {
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages is already running with pid $spid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \
+ $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \
+ >> $gitlab_pages_log 2>&1 &
+ fi
+ fi
+
# Wait for the pids to be planted
wait_for_pids
# Finally check the status to tell wether or not GitLab is running
@@ -278,13 +318,17 @@ stop_gitlab() {
echo "Shutting down GitLab MailRoom"
RAILS_ENV=$RAILS_ENV bin/mail_room stop
fi
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "Shutting down gitlab-pages"
+ kill -- $(cat $gitlab_pages_pid_path)
+ fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -298,6 +342,7 @@ stop_gitlab() {
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
+ rm -f "$gitlab_pages_pid_path"
print_status
}
@@ -305,7 +350,7 @@ stop_gitlab() {
## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -331,7 +376,14 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages with pid $mpid is running."
+ else
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -362,7 +414,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index cc8617b72ca..e5797d8fe3c 100755..100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+# The GitLab Pages Daemon needs either a separate IP address on which it will
+# listen or use different ports than 80 or 443 that will be forwarded to GitLab
+# Pages Daemon.
+#
+# To enable HTTP support for custom domains add the `-listen-http` directive
+# in `gitlab_pages_options` below.
+# 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
+#
+# 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
+#
+# 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.
+gitlab_pages_enabled=false
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
+
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
# The default is "false"
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
new file mode 100644
index 00000000000..d9746c5c1aa
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages
@@ -0,0 +1,28 @@
+## GitLab
+##
+
+## Pages serving host
+server {
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
new file mode 100644
index 00000000000..a1ccf266835
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -0,0 +1,77 @@
+## GitLab
+##
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ ## Either remove "default_server" from the listen line below,
+ ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
+ ## to be served if you visit any address that your server responds to, eg.
+ ## the ip address of the server (http://x.x.x.x/)
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ return 301 https://$http_host$request_uri;
+
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_access.log;
+}
+
+## Pages serving host
+server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl http2;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/gitlab-pages.crt;
+ ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key;
+
+ # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs
+ ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ ## See app/controllers/application_controller.rb for headers set
+
+ ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
+ ## Replace with your ssl_trusted_certificate. For more info see:
+ ## - https://medium.com/devops-programming/4445f4862461
+ ## - https://www.ruby-forum.com/topic/4419319
+ ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
+ # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt;
+
+ ## [Optional] Generate a stronger DHE parameter:
+ ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
+ ##
+ # ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5661394058d..330031aaddc 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -82,6 +82,9 @@ server {
##
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
+ ## [Optional] Enable HTTP Strict Transport Security
+ # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+
## 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/config_lint.rake b/lib/tasks/config_lint.rake
new file mode 100644
index 00000000000..ddbcf1e1eb8
--- /dev/null
+++ b/lib/tasks/config_lint.rake
@@ -0,0 +1,25 @@
+module ConfigLint
+ def self.run(files)
+ failures = files.reject do |file|
+ yield(file)
+ end
+
+ if failures.present?
+ puts failures
+ exit failures.count
+ end
+ end
+end
+
+desc "Checks syntax for shell scripts and nginx config files in 'lib/support/'"
+task :config_lint do
+ shell_scripts = [
+ 'lib/support/init.d/gitlab',
+ 'lib/support/init.d/gitlab.default.example',
+ 'lib/support/deploy/deploy.sh'
+ ]
+
+ ConfigLint.run(shell_scripts) do |file|
+ Kernel.system('bash', '-n', file)
+ end
+end
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/eslint.rake b/lib/tasks/eslint.rake
index d43cbad1909..51f5d768102 100644
--- a/lib/tasks/eslint.rake
+++ b/lib/tasks/eslint.rake
@@ -1,7 +1,8 @@
unless Rails.env.production?
desc "GitLab | Run ESLint"
- task :eslint do
- system("npm", "run", "eslint")
+ task eslint: ['yarn:check'] do
+ unless system('yarn run eslint')
+ abort('rake eslint failed')
+ end
end
end
-
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 5d884bf9f66..098f9851b45 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -1,25 +1,26 @@
namespace :gitlab do
namespace :assets do
desc 'GitLab | Assets | Compile all frontend assets'
- task :compile do
- Rake::Task['assets:precompile'].invoke
- Rake::Task['gitlab:assets:fix_urls'].invoke
- end
+ task compile: [
+ 'yarn:check',
+ 'assets:precompile',
+ 'webpack:compile',
+ 'gitlab:assets:fix_urls'
+ ]
desc 'GitLab | Assets | Clean up old compiled frontend assets'
- task :clean do
- Rake::Task['assets:clean'].invoke
- end
+ task clean: ['assets:clean']
desc 'GitLab | Assets | Remove all compiled frontend assets'
- task :purge do
- Rake::Task['assets:clobber'].invoke
- end
+ task purge: ['assets:clobber']
+
+ desc 'GitLab | Assets | Uninstall frontend dependencies'
+ task purge_modules: ['yarn:clobber']
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/backup.rake b/lib/tasks/gitlab/backup.rake
index a9f1255e8cf..1650263b98d 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:pages:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
@@ -56,6 +57,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?('pages')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
@@ -159,6 +161,25 @@ namespace :gitlab do
end
end
+ namespace :pages do
+ task create: :environment do
+ $progress.puts "Dumping pages ... ".color(:blue)
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("pages")
+ $progress.puts "[SKIPPED]".color(:cyan)
+ else
+ Backup::Pages.new.dump
+ $progress.puts "done".color(:green)
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new.restore
+ $progress.puts "done".color(:green)
+ end
+ end
+
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 35c4194e87c..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
########################
@@ -724,8 +718,11 @@ namespace :gitlab do
def check_imap_authentication
print "IMAP server credentials are correct? ... "
- config_path = Rails.root.join('config', 'mail_room.yml')
- config_file = YAML.load(ERB.new(File.read(config_path)).result)
+ config_path = Rails.root.join('config', 'mail_room.yml').to_s
+ erb = ERB.new(File.read(config_path))
+ erb.filename = config_path
+ config_file = YAML.load(erb.result)
+
config = config_file[:mailboxes].first
if config
@@ -754,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
@@ -802,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
@@ -817,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, '*'))
@@ -835,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
@@ -852,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)
@@ -914,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} ? ... "
@@ -985,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 4a696a52b4d..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,17 +48,18 @@ 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')
- next if Project.find_with_namespace(repo_with_namespace)
+ 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
File.rename(path, new_path)
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/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 036a9307ab5..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|
@@ -29,7 +30,7 @@ namespace :gitlab do
next
end
- project = Project.find_with_namespace(path)
+ project = Project.find_by_full_path(path)
if project
puts " * #{project.name} (#{repo_path}) exists"
@@ -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 7e2a6668e59..6cbc83b8973 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -1,13 +1,13 @@
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
- project_path = Project.find_with_namespace(args.project).repository.path_to_repo
+ project_path = Project.find_by_full_path(args.project).repository.path_to_repo
Sidekiq.redis do |redis|
unless redis.exists(QUEUE)
@@ -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 4d4e746503a..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 teaspoon)
+ %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/grape.rake b/lib/tasks/grape.rake
index 9980e0b7984..ea2698da606 100644
--- a/lib/tasks/grape.rake
+++ b/lib/tasks/grape.rake
@@ -2,7 +2,11 @@ namespace :grape do
desc 'Print compiled grape routes'
task routes: :environment do
API::API.routes.each do |route|
- puts route
+ puts "#{route.options[:method]} #{route.path} - #{route_description(route.options)}"
end
end
+
+ def route_description(options)
+ options[:settings][:description][:description] if options[:settings][:description]
+ end
end
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
new file mode 100644
index 00000000000..40465ea3bf0
--- /dev/null
+++ b/lib/tasks/karma.rake
@@ -0,0 +1,20 @@
+unless Rails.env.production?
+ namespace :karma do
+ desc 'GitLab | Karma | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures) do |t|
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.rspec_opts = '--format documentation'
+ end
+
+ desc 'GitLab | Karma | Run JavaScript tests'
+ task tests: ['yarn:check'] do
+ sh "yarn run karma" do |ok, res|
+ abort('rake karma:tests failed') unless ok
+ end
+ end
+ end
+
+ desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests'
+ task karma: ['karma:fixtures', 'karma:tests']
+end
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/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
deleted file mode 100644
index 08caedd7ff3..00000000000
--- a/lib/tasks/teaspoon.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-unless Rails.env.production?
- Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
-
- namespace :teaspoon do
- desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
- ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
- t.rspec_opts = '--format documentation'
- end
-
- desc 'GitLab | Teaspoon | Run JavaScript tests'
- task :tests do
- require "teaspoon/console"
- options = {}
- abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
- end
- end
-
- desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
- task :teaspoon do
- Rake::Task['teaspoon:fixtures'].invoke
- Rake::Task['teaspoon:tests'].invoke
- end
-end
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d3dcbd2c29b..3e01f91d32c 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -7,5 +7,5 @@ end
unless Rails.env.production?
desc "GitLab | Run all tests on CI with simplecov"
- task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec]
+ task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec]
end
diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake
new file mode 100644
index 00000000000..2ac88a039e7
--- /dev/null
+++ b/lib/tasks/yarn.rake
@@ -0,0 +1,40 @@
+
+namespace :yarn do
+ desc 'Ensure Yarn is installed'
+ task :available do
+ unless system('yarn --version', out: File::NULL)
+ warn(
+ 'Error: Yarn executable was not detected in the system.'.color(:red),
+ 'Download Yarn at https://yarnpkg.com/en/docs/install'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Ensure Node dependencies are installed'
+ task check: ['yarn:available'] do
+ unless system('yarn check --ignore-engines', out: File::NULL)
+ warn(
+ 'Error: You have unmet dependencies. (`yarn check` command failed)'.color(:red),
+ 'Run `yarn install` to install missing modules.'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Install Node dependencies with Yarn'
+ task install: ['yarn:available'] do
+ unless system('yarn install --pure-lockfile --ignore-engines')
+ abort 'Error: Unable to install node modules.'.color(:red)
+ end
+ end
+
+ desc 'Remove Node dependencies'
+ task :clobber do
+ warn 'Purging ./node_modules directory'.color(:red)
+ FileUtils.rm_rf 'node_modules'
+ end
+end
+
+desc 'Install Node dependencies with Yarn'
+task yarn: ['yarn:install']
diff --git a/package.json b/package.json
index 49b8210e427..9652dd8f972 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,69 @@
{
"private": true,
"scripts": {
+ "dev-server": "webpack-dev-server --config config/webpack.config.js",
"eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
- "eslint-fix": "npm run eslint -- --fix",
- "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
+ "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 .",
+ "karma": "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"
+ },
+ "dependencies": {
+ "babel-core": "^6.22.1",
+ "babel-loader": "^6.2.10",
+ "babel-preset-es2015": "^6.22.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",
+ "emoji-unicode-version": "^0.2.1",
+ "jquery": "^2.2.1",
+ "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.1.10",
+ "vue-resource": "^0.9.3",
+ "webpack": "^2.2.1",
+ "webpack-bundle-analyzer": "^2.3.0"
},
"devDependencies": {
+ "babel-plugin-istanbul": "^4.0.0",
"eslint": "^3.10.1",
"eslint-config-airbnb-base": "^10.0.1",
+ "eslint-import-resolver-webpack": "^0.8.1",
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
- "istanbul": "^0.4.5"
+ "istanbul": "^0.4.5",
+ "jasmine-core": "^2.5.2",
+ "jasmine-jquery": "^2.1.1",
+ "karma": "^1.4.1",
+ "karma-coverage-istanbul-reporter": "^0.2.0",
+ "karma-jasmine": "^1.1.0",
+ "karma-mocha-reporter": "^2.2.2",
+ "karma-phantomjs-launcher": "^1.0.2",
+ "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/.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..2814a7bdef0
--- /dev/null
+++ b/qa/Dockerfile
@@ -0,0 +1,14 @@
+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 ./ ./
+RUN bundle install
+
+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/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..fe80deb6429
--- /dev/null
+++ b/qa/qa/page/main/entry.rb
@@ -0,0 +1,26 @@
+module QA
+ module Page
+ module Main
+ class Entry < Page::Base
+ def initialize
+ visit('/')
+
+ # This resolves cold boot problems with login page
+ find('.application', wait: 120)
+ 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 4a63c760744..e157d8e0791 100644
--- a/rubocop/cop/gem_fetcher.rb
+++ b/rubocop/cop/gem_fetcher.rb
@@ -1,17 +1,15 @@
module RuboCop
module Cop
- # Cop that checks for all gems specified in the Gemfile, and will
- # alert if any gem is to be fetched not from the RubyGems index.
- # This enforcement is done so as to minimize external build
- # dependencies and build times.
+ # This cop prevents usage of the `git` and `github` arguments to `gem` in a
+ # `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)
- file_path = node.location.expression.source_buffer.name
- return unless file_path.end_with?("Gemfile")
+ return unless gemfile?(node)
func_name = node.children[1]
return unless func_name == :gem
@@ -19,10 +17,21 @@ module RuboCop
node.children.last.each_node(:pair) do |pair|
key_name = pair.children[0].children[0].to_sym
if GIT_KEYS.include?(key_name)
- add_offense(node, :selector)
+ add_offense(node, pair.source_range, MSG)
end
end
end
+
+ private
+
+ def gemfile?(node)
+ node
+ .location
+ .expression
+ .source_buffer
+ .name
+ .end_with?("Gemfile")
+ end
end
end
end
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
new file mode 100644
index 00000000000..d2cf36c454a
--- /dev/null
+++ b/rubocop/cop/migration/add_column.rb
@@ -0,0 +1,52 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if columns are added in a way that doesn't require
+ # downtime.
+ class AddColumn < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ WHITELISTED_TABLES = [:application_settings].freeze
+
+ MSG = '`add_column` with a default value requires downtime, ' \
+ 'use `add_column_with_default` instead'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_column
+
+ # Ignore whitelisted tables.
+ return if table_whitelisted?(node.children[2])
+
+ opts = node.children.last
+
+ return unless opts && opts.type == :hash
+
+ opts.each_node(:pair) do |pair|
+ if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
+ add_offense(node, :selector)
+ end
+ end
+ end
+
+ def table_whitelisted?(symbol)
+ symbol && symbol.type == :sym &&
+ WHITELISTED_TABLES.include?(symbol.children[0])
+ end
+
+ def hash_key_type(pair)
+ pair.children[0].type
+ end
+
+ def hash_key_name(pair)
+ pair.children[0].children[0]
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
new file mode 100644
index 00000000000..54a920d4b49
--- /dev/null
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -0,0 +1,34 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_column_with_default` is used with `up`/`down` methods
+ # and not `change`.
+ class AddColumnWithDefault < RuboCop::Cop::Cop
+ 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`'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_column_with_default
+
+ 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_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
new file mode 100644
index 00000000000..d1fc94d55be
--- /dev/null
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -0,0 +1,27 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_concurrent_foreign_key` is used instead of
+ # `add_foreign_key`.
+ class AddConcurrentForeignKey < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ add_offense(node, :selector) if name == :add_foreign_key
+ end
+
+ def method_name(node)
+ node.children.first
+ end
+ end
+ end
+ end
+end
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 d9247a1f7ea..fa21a0d6555 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -1,3 +1,5 @@
+require_relative '../../migration_helpers'
+
module RuboCop
module Cop
module Migration
@@ -5,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/cop/migration/column_with_default.rb b/rubocop/cop/migration/column_with_default.rb
deleted file mode 100644
index 97ee8b11044..00000000000
--- a/rubocop/cop/migration/column_with_default.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module RuboCop
- module Cop
- module Migration
- # Cop that checks if columns are added in a way that doesn't require
- # downtime.
- class ColumnWithDefault < RuboCop::Cop::Cop
- include MigrationHelpers
-
- WHITELISTED_TABLES = [:application_settings]
-
- MSG = 'add_column with a default value requires downtime, ' \
- 'use add_column_with_default instead'
-
- def on_send(node)
- return unless in_migration?(node)
-
- name = node.children[1]
-
- return unless name == :add_column
-
- # Ignore whitelisted tables.
- return if table_whitelisted?(node.children[2])
-
- opts = node.children.last
-
- return unless opts && opts.type == :hash
-
- opts.each_node(:pair) do |pair|
- if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
- add_offense(node, :selector)
- end
- end
- end
-
- def table_whitelisted?(symbol)
- symbol && symbol.type == :sym &&
- WHITELISTED_TABLES.include?(symbol.children[0])
- end
-
- def hash_key_type(pair)
- pair.children[0].type
- end
-
- def hash_key_name(pair)
- pair.children[0].children[0]
- end
- end
- end
- end
-end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7f20754ee51..a50a522cf9d 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,4 +1,7 @@
-require_relative 'migration_helpers'
-require_relative 'cop/migration/add_index'
-require_relative 'cop/migration/column_with_default'
+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/shared/pages/.gitkeep b/shared/pages/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/pages/.gitkeep
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 294fae95752..0b8ff006d22 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -8,7 +8,7 @@ describe 'mail_room.yml' do
context 'when incoming email is disabled' do
before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s
Gitlab::MailRoom.reset_config!
end
@@ -26,7 +26,7 @@ describe 'mail_room.yml' do
let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s
Gitlab::MailRoom.reset_config!
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/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
new file mode 100644
index 00000000000..b5fe40d0510
--- /dev/null
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Admin::RunnersController do
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe '#index' do
+ it 'lists all runners' do
+ get :index
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe '#show' do
+ it 'shows a particular runner' do
+ get :show, id: runner.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'shows 404 for unknown runner' do
+ get :show, id: 0
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, id: runner.id, runner: { description: new_desc }
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, id: runner.id
+
+ expect(response).to have_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, id: runner.id
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, id: runner.id
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(false)
+ 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 79ef3a1adad..7072bd5e87c 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -1,16 +1,19 @@
require 'spec_helper'
describe Dashboard::TodosController do
+ include ApiHelpers
+
let(:user) { create(:user) }
+ let(:author) { create(:user) }
let(:project) { create(:empty_project) }
let(:todo_service) { TodoService.new }
- describe 'GET #index' do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+ describe 'GET #index' do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
@@ -34,4 +37,16 @@ describe Dashboard::TodosController do
end
end
end
+
+ describe 'PATCH #restore' do
+ let(:todo) { create(:todo, :done, user: user, project: project, author: author) }
+
+ it 'restores the todo to pending state' do
+ patch :restore, id: todo.id
+
+ expect(todo.reload).to be_pending
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
+ end
+ end
end
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
new file mode 100644
index 00000000000..566d8515198
--- /dev/null
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DashboardController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET issues' do
+ it_behaves_like 'issuables list meta-data', :issue, :issues
+ end
+
+ describe 'GET merge requests' do
+ it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
+ 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 6bcfae0fc13..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
@@ -42,10 +32,9 @@ describe Profiles::KeysController do
end
describe "user with keys" do
- before do
- user.keys << create(:key)
- user.keys << create(:another_key)
- end
+ let!(:key) { create(:key, user: user) }
+ let!(:another_key) { create(:another_key, user: user) }
+ let!(:deploy_key) { create(:deploy_key, user: user) }
it "does generally work" do
get :get_keys, username: user.username
@@ -53,16 +42,16 @@ describe Profiles::KeysController do
expect(response).to be_success
end
- it "renders all keys separated with a new line" do
+ it "renders all non deploy keys separated with a new line" do
get :get_keys, username: user.username
- expect(response.body).not_to eq("")
+ expect(response.body).not_to eq('')
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
- # Unique part of key 1
- expect(response.body).to match(/PWx6WM4lhHNedGfBpPJNPpZ/)
- # Key 2
- expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/)
+ expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
+ expect(response.body).to include(another_key.key)
+
+ expect(response.body).not_to include(deploy_key.key)
end
it "does not render the comment of the key" do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
new file mode 100644
index 00000000000..58caf7999cf
--- /dev/null
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -0,0 +1,45 @@
+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/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 8f02003992a..7b3aa0491c7 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -25,8 +25,7 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
- dashboard: 'stars',
- theme_id: '1'
+ dashboard: 'stars'
)
patch :update, user: params, format: format
@@ -41,8 +40,7 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
- dashboard: 'stars',
- theme_id: '2'
+ dashboard: 'stars'
}.with_indifferent_access
expect(user).to receive(:update_attributes).with(prefs)
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 b36d0e69330..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',
@@ -86,32 +86,47 @@ describe Projects::BlobController do
end
context 'when user has forked project' do
- let(:guest) { create(:user) }
- let!(:forked_project) { Projects::ForkService.new(project, guest).execute }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") }
-
- before { sign_in(guest) }
-
- it "redirects to forked project new merge request" do
- default_params[:target_branch] = "fork-test-1"
- default_params[:create_merge_request] = 1
-
- allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success)
-
- put :update, default_params
-
- expect(response).to redirect_to(
- new_namespace_project_merge_request_path(
- forked_project.namespace,
- forked_project,
- merge_request: {
- source_project_id: forked_project.id,
- target_project_id: project.id,
- source_branch: "fork-test-1",
- target_branch: "master"
- }
+ let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
+ let!(:forked_project) { forked_project_link.forked_to_project }
+ let(:guest) { forked_project.owner }
+
+ before do
+ sign_in(guest)
+ end
+
+ context 'when editing on the fork' do
+ before do
+ default_params[:namespace_id] = forked_project.namespace
+ default_params[:project_id] = forked_project
+ end
+
+ it 'redirects to blob' do
+ put :update, default_params
+
+ expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG'))
+ end
+ end
+
+ context 'when editing on the original repository' do
+ it "redirects to forked project new merge request" do
+ default_params[:target_branch] = "fork-test-1"
+ default_params[:create_merge_request] = 1
+
+ put :update, default_params
+
+ expect(response).to redirect_to(
+ new_namespace_project_merge_request_path(
+ forked_project.namespace,
+ forked_project,
+ merge_request: {
+ source_project_id: forked_project.id,
+ target_project_id: project.id,
+ source_branch: "fork-test-1",
+ target_branch: "master"
+ }
+ )
)
- )
+ end
end
end
end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 299d2c981d3..15667e8d4b1 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do
end
describe 'GET index' do
- context 'with valid list id' do
- it 'returns issues that have the list label applied' do
- johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
- issue = create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
- create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- issue.subscribe(johndoe, project)
-
- list_issues user: user, board: board, list: list2
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('issues')
- expect(parsed_response.length).to eq 2
- end
- end
+ let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
context 'with invalid board id' do
it 'returns a not found 404 response' do
@@ -44,11 +28,48 @@ describe Projects::Boards::IssuesController do
end
end
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- list_issues user: user, board: board, list: 999
+ context 'when list id is present' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ issue = create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
+ create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ issue.subscribe(johndoe, project)
- expect(response).to have_http_status(404)
+ list_issues user: user, board: board, list: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ 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
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: board, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when list id is missing' do
+ it 'returns opened issues without board labels applied' do
+ bug = create(:label, project: project, name: 'Bug')
+ create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [bug])
+
+ list_issues user: user, board: board
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
end
end
@@ -65,13 +86,17 @@ describe Projects::Boards::IssuesController do
end
end
- def list_issues(user:, board:, list:)
+ def list_issues(user:, board:, list: nil)
sign_in(user)
- get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- board_id: board.to_param,
- list_id: list.to_param
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ list_id: list.try(:to_param)
+ }
+
+ get :index, params.compact
end
end
@@ -122,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 },
@@ -185,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 34d6119429d..432f3c53c90 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('lists')
- expect(parsed_response.length).to eq 3
+ expect(parsed_response.length).to eq 2
end
context 'with unauthorized user' do
@@ -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 7ac1d62d1b1..83d80b376fb 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -3,9 +3,12 @@ require 'spec_helper'
describe Projects::EnvironmentsController do
include ApiHelpers
- let(:environment) { create(:environment) }
- let(:project) { environment.project }
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:environment) do
+ create(:environment, name: 'production', project: project)
+ end
before do
project.team << [user, :master]
@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do
end
end
- context 'when requesting JSON response' do
- it 'responds with correct JSON' do
- get :index, environment_params(format: :json)
+ context 'when requesting JSON response for folders' do
+ before do
+ create(:environment, project: project,
+ name: 'staging/review-1',
+ state: :available)
+
+ create(:environment, project: project,
+ name: 'staging/review-2',
+ state: :available)
+
+ create(:environment, project: project,
+ name: 'staging/review-3',
+ state: :stopped)
+ end
- first_environment = json_response.first
+ let(:environments) { json_response['environments'] }
- expect(first_environment).not_to be_empty
- expect(first_environment['name']). to eq environment.name
+ context 'when requesting available environments scope' do
+ before do
+ get :index, environment_params(format: :json, scope: :available)
+ end
+
+ it 'responds with a payload describing available environments' do
+ expect(environments.count).to eq 2
+ expect(environments.first['name']).to eq 'production'
+ expect(environments.second['name']).to eq 'staging'
+ expect(environments.second['size']).to eq 2
+ expect(environments.second['latest']['name']).to eq 'staging/review-2'
+ end
+
+ it 'contains values describing environment scopes sizes' do
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
+ end
+
+ context 'when requesting stopped environments scope' do
+ before do
+ get :index, environment_params(format: :json, scope: :stopped)
+ end
+
+ it 'responds with a payload describing stopped environments' do
+ expect(environments.count).to eq 1
+ expect(environments.first['name']).to eq 'staging'
+ expect(environments.first['size']).to eq 1
+ expect(environments.first['latest']['name']).to eq 'staging/review-3'
+ end
+
+ it 'contains values describing environment scopes sizes' do
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
end
end
end
@@ -140,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 5f27f336f72..6ceaf96f78f 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
@@ -24,14 +24,16 @@ describe Projects::IssuesController do
project.team << [user, :developer]
end
+ 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
@@ -40,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
@@ -48,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
@@ -65,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]))
@@ -74,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)
@@ -92,7 +94,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
@@ -102,7 +104,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
@@ -115,7 +126,7 @@ describe Projects::IssuesController do
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
@@ -123,14 +134,16 @@ describe Projects::IssuesController do
end
describe 'PUT #update' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'update invalid issuable', Issue
+
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
-
context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] }
@@ -150,10 +163,117 @@ describe Projects::IssuesController do
end
end
+ context 'Akismet is enabled' do
+ let(:project) { create(:project_empty_repo, :public) }
+
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when an issue is not identified as spam' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
+
+ it 'normally updates the issue' do
+ expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when an issue is identified as spam' do
+ before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
+
+ context 'when captcha is not verified' do
+ def update_spam_issue
+ update_issue(title: 'Spam Title', description: 'Spam lives here')
+ end
+
+ before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
+
+ it 'rejects an issue recognized as a spam' do
+ expect { update_spam_issue }.not_to change{ issue.reload.title }
+ end
+
+ it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { update_spam_issue }.not_to change{ issue.reload.title }
+ end
+
+ it 'creates a spam log' do
+ update_spam_issue
+
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs.first.title).to eq('Spam Title')
+ expect(spam_logs.first.recaptcha_verified).to be_falsey
+ end
+
+ it 'renders verify template' do
+ update_spam_issue
+
+ expect(response).to render_template(:verify)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let(:spammy_title) { 'Whatever' }
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
+
+ def update_verified_issue
+ update_issue({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+ end
+
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha)
+ .and_return(true)
+ end
+
+ it 'redirect to issue page' do
+ update_verified_issue
+
+ expect(response).
+ to redirect_to(namespace_project_issue_path(project.namespace, project, issue))
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect{ update_verified_issue }.to change{ issue.reload.title }.to(spammy_title)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ end
+
+ it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+ spam_log = create(:spam_log)
+
+ expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }.
+ not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
+ end
+ end
+
+ def update_issue(issue_params = {}, additional_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: issue.iid,
+ issue: issue_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
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
@@ -233,7 +353,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
@@ -296,7 +416,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
@@ -307,7 +427,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
@@ -318,7 +438,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
@@ -326,16 +446,16 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
- def post_new_issue(attrs = {})
+ def post_new_issue(issue_attrs = {}, additional_params = {})
sign_in(user)
project = create(:empty_project, :public)
project.team << [user, :developer]
post :create, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- issue: { title: 'Title', description: 'Description' }.merge(attrs)
- }
+ project_id: project,
+ issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
+ }.merge(additional_params)
project.issues.first
end
@@ -351,11 +471,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
@@ -374,28 +494,106 @@ 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
before do
+ stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
- def post_spam_issue
- post_new_issue(title: 'Spam Title', description: 'Spam lives here')
- end
+ context 'when an issue is not identified as spam' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
- it 'rejects an issue recognized as spam' do
- expect{ post_spam_issue }.not_to change(Issue, :count)
- expect(response).to render_template(:new)
+ it 'does not create an issue' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
end
- it 'creates a spam log' do
- post_spam_issue
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('Spam Title')
+ context 'when an issue is identified as spam' do
+ before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
+
+ context 'when captcha is not verified' do
+ def post_spam_issue
+ post_new_issue(title: 'Spam Title', description: 'Spam lives here')
+ end
+
+ before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
+
+ it 'rejects an issue recognized as a spam' do
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+
+ it 'creates a spam log' do
+ post_spam_issue
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs.first.title).to eq('Spam Title')
+ expect(spam_logs.first.recaptcha_verified).to be_falsey
+ end
+
+ it 'does not create an issue when it is not valid' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
+
+ it 'does not create an issue when recaptcha is not enabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
+
+ def post_verified_issue
+ post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
+ end
+
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect { post_verified_issue }.to change(Issue, :count)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ end
+
+ it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+ spam_log = create(:spam_log)
+
+ expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }.
+ not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
end
end
@@ -405,7 +603,7 @@ describe Projects::IssuesController do
end
it 'creates a user agent detail' do
- expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
end
end
@@ -441,8 +639,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
@@ -458,7 +656,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
@@ -471,7 +669,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
@@ -480,7 +678,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
@@ -493,8 +691,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 e019541e74f..250d64f7055 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -22,30 +22,50 @@ describe Projects::MergeRequestsController do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
+ before { fork_project.team << [user, :master] }
- before do
- fork_project.team << [user, :master]
+ context 'when rendering HTML response' do
+ it 'renders new merge request widget template' do
+ submit_new_merge_request
+
+ expect(response).to be_success
+ end
end
- it 'renders it' do
- get :new,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
- merge_request: {
- source_branch: 'remove-submodule',
- target_branch: 'master'
- }
+ context 'when rendering JSON response' do
+ before do
+ create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
+ ref: 'remove-submodule',
+ project: fork_project)
+ end
- expect(response).to be_success
+ it 'renders JSON including serialized pipelines' do
+ submit_new_merge_request(format: :json)
+
+ expect(response).to be_ok
+ expect(json_response).to have_key 'pipelines'
+ expect(json_response['pipelines']).not_to be_empty
+ end
end
end
+
+ def submit_new_merge_request(format: :html)
+ get :new,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project,
+ merge_request: {
+ source_branch: 'remove-submodule',
+ target_branch: 'master'
+ },
+ format: format
+ end
end
shared_examples "loads labels" do |action|
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
@@ -57,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)
@@ -71,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
@@ -79,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)
@@ -92,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)
@@ -107,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)
@@ -119,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)
@@ -129,13 +149,17 @@ describe Projects::MergeRequestsController do
end
describe 'GET index' do
+ let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+
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
+ it_behaves_like "issuables list meta-data", :merge_request
+
context 'when page param' do
let(:last_page) { project.merge_requests.page().total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -193,8 +217,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'
@@ -208,8 +232,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'
@@ -223,8 +247,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'
@@ -232,14 +256,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'
}
@@ -294,41 +320,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
@@ -403,7 +429,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
@@ -416,7 +442,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
@@ -425,7 +451,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
@@ -434,7 +460,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
}
@@ -514,7 +540,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)
@@ -578,7 +604,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
@@ -644,7 +670,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
@@ -684,20 +710,13 @@ 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
- it 'responds with a rendered HTML partial' do
- expect(response)
- .to render_template('projects/merge_requests/show/_pipelines')
- expect(json_response).to have_key 'html'
- end
-
it 'responds with serialized pipelines' do
- expect(json_response).to have_key 'pipelines'
- expect(json_response['pipelines']).not_to be_empty
+ expect(json_response).not_to be_empty
end
end
end
@@ -710,7 +729,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
@@ -728,7 +747,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
@@ -757,7 +776,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?
@@ -791,7 +810,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)
@@ -802,7 +821,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,
@@ -858,7 +877,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,
@@ -1009,7 +1028,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
@@ -1064,7 +1083,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
@@ -1077,8 +1096,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
}
@@ -1116,27 +1135,27 @@ 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
- context 'when no special status for MR' do
+ context 'when MR does not have special state' do
let(:merge_request) { create(:merge_request, source_project: project) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
- it 'sets status to nil' do
- expect(assigns(:status)).to be_nil
+ it 'sets status to success' do
+ expect(assigns(:status)).to eq(:success)
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/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
new file mode 100644
index 00000000000..2362df895a8
--- /dev/null
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Projects::PagesDomainsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET show' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "displays the 'show' page" do
+ get(:show, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'GET new' do
+ it "displays the 'new' page" do
+ get(:new, request_params)
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('new')
+ end
+ end
+
+ describe 'POST create' do
+ let(:pages_domain_params) do
+ build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain)
+ end
+
+ it "creates a new pages domain" do
+ expect do
+ post(:create, request_params.merge(pages_domain: pages_domain_params))
+ end.to change { PagesDomain.count }.by(1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "deletes the pages domain" do
+ expect do
+ delete(:destroy, request_params.merge(id: pages_domain.domain))
+ end.to change { PagesDomain.count }.by(-1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ 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/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
new file mode 100644
index 00000000000..0fa249e4405
--- /dev/null
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Projects::RunnersController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:runner) { create(:ci_runner) }
+
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: runner
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.add_master(user)
+ project.runners << runner
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, params.merge(runner: { description: new_desc } )
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, params
+
+ expect(response).to have_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(false)
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
new file mode 100644
index 00000000000..e9a91cff1b3
--- /dev/null
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -0,0 +1,20 @@
+require('spec_helper')
+
+describe Projects::Settings::CiCdController do
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ 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/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 19e948d8fb8..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)
@@ -70,16 +70,16 @@ describe Projects::SnippetsController do
end
describe 'POST #create' do
- def create_snippet(project, snippet_params = {})
+ def create_snippet(project, snippet_params = {}, additional_params = {})
sign_in(user)
project.add_developer(user)
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
context 'when the snippet is spam' do
@@ -87,35 +87,179 @@ describe Projects::SnippetsController do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
- context 'when the project is private' do
- let(:private_project) { create(:project_empty_repo, :private) }
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, 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(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
- context 'when the snippet is public' do
- it 'creates the snippet' do
- expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
- to change { Snippet.count }.by(1)
+ it 'renders :new with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ create_snippet(project, visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:new)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ create_snippet(project, visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ create_snippet(project,
+ { visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(Snippet.last)
end
end
end
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:project) { create :project, :public }
+ let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level }
+
+ def update_snippet(snippet_params = {}, additional_params = {})
+ sign_in(user)
+
+ project.add_developer(user)
+
+ put :update, {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: snippet.id,
+ project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }.merge(additional_params)
+
+ snippet.reload
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
- context 'when the project is public' do
- context 'when the snippet is private' do
- it 'creates the snippet' do
- expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
- to change { Snippet.count }.by(1)
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
end
end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
- context 'when the snippet is public' do
- it 'rejects the shippet' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
- not_to change { Snippet.count }
- expect(response).to render_template(:new)
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
end
- it 'creates a spam log' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
- to change { SpamLog.count }.by(1)
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
end
end
end
@@ -137,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
@@ -156,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
@@ -166,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)
@@ -177,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)
@@ -188,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
@@ -198,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
@@ -206,4 +350,37 @@ describe Projects::SnippetsController do
end
end
end
+
+ describe 'GET #raw' do
+ let(:project_snippet) do
+ create(
+ :project_snippet, :public,
+ project: project,
+ author: user,
+ content: "first line\r\nsecond line\r\nthird line"
+ )
+ end
+
+ context 'CRLF line ending' do
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: project_snippet.to_param
+ }
+ end
+
+ it 'returns LF line endings by default' do
+ get :raw, params
+
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
+
+ it 'does not convert line endings when parameter present' do
+ get :raw, params.merge(line_ending: :raw)
+
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
+ end
+ end
+ end
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 99d0bcfa8d1..70e7f9ca96e 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -14,12 +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", "test 3", "master", 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")
@@ -28,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 f1c8891e87a..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
@@ -170,68 +183,24 @@ describe Projects::UploadsController do
project.team << [user, :master]
end
- context "when the user is blocked" do
+ context "when the file exists" do
before do
- user.block
- project.team << [user, :master]
- end
-
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- context "when the file is an image" do
- before do
- allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
end
- context "when the file doesn't exist" do
- it "redirects to the sign in page" do
- go
+ it "responds with status 200" do
+ go
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to have_http_status(200)
end
end
- context "when the user isn't blocked" do
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
new file mode 100644
index 00000000000..e3f3b4fe8eb
--- /dev/null
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Projects::VariablesController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ 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,
+ variable: { key: "one", value: "two" }
+
+ expect(flash[:notice]).to include 'Variables were successfully updated.'
+ expect(response).to redirect_to(namespace_project_settings_ci_cd_path(project.namespace, project))
+ end
+ end
+
+ context 'variable is invalid' do
+ it 'shows an alert flash message' do
+ post :create, namespace_id: project.namespace.to_param, project_id: project,
+ variable: { key: "..one", value: "two" }
+
+ expect(response).to render_template("projects/variables/show")
+ end
+ end
+ end
+
+ describe 'POST #update' do
+ let(:variable) { create(:ci_variable) }
+
+ context 'updating a variable with valid characters' do
+ before do
+ variable.gl_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,
+ id: variable.id, variable: { key: variable.key, value: 'two' }
+
+ expect(flash[:notice]).to include 'Variable was successfully updated.'
+ expect(response).to redirect_to(namespace_project_variables_path(project.namespace, project))
+ end
+
+ it 'renders the action #show if the variable key is invalid' do
+ 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)
+ expect(response).to render_template :show
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 9323f723bdb..a1ec41322ad 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,7 +71,7 @@ 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
@@ -79,7 +79,7 @@ describe ProjectsController do
it 'shows issues list page if wiki is disabled' do
project.project_feature.update_attribute(:wiki_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/issues/_issues')
end
@@ -88,7 +88,7 @@ describe ProjectsController 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 +96,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 +113,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 +133,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 +154,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 +170,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 +179,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,11 +200,22 @@ 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
end
+
+ context "redirection from http://someproject.git" do
+ it 'redirects to project page (format.html)' do
+ project = create(:project, :public)
+
+ 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)
+ end
+ end
end
describe "#update" do
@@ -228,7 +231,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
@@ -246,7 +249,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)
@@ -266,7 +269,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
@@ -276,8 +279,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
@@ -305,23 +308,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
@@ -355,8 +358,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)
@@ -366,8 +369,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
@@ -376,7 +379,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")
@@ -385,7 +388,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/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 42fbfe89368..8cc216445eb 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -44,7 +44,7 @@ describe RegistrationsController do
post(:create, user_params)
expect(response).to render_template(:new)
- expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
it 'redirects to the dashboard when the recaptcha is solved' do
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/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index b7bb9290712..3173aae664c 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe SearchController do
let(:user) { create(:user) }
- let(:project) { create(:empty_project, :public) }
before do
sign_in(user)
@@ -22,7 +21,7 @@ describe SearchController do
before { sign_out(user) }
it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -31,17 +30,8 @@ describe SearchController do
end
end
- it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
- note = create(:note_on_issue, project: project)
-
- get :show, project_id: project.id, scope: 'notes', search: note.note
-
- expect(assigns[:search_objects].count).to eq(0)
- end
-
it "doesn't expose comments on merge_requests" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -50,7 +40,7 @@ describe SearchController do
end
it "doesn't expose comments on snippets" do
- project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
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/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index dadcb90cfc2..5de3b9890ef 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -139,12 +139,14 @@ describe SnippetsController do
end
describe 'POST #create' do
- def create_snippet(snippet_params = {})
+ def create_snippet(snippet_params = {}, additional_params = {})
sign_in(user)
post :create, {
personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
- }
+ }.merge(additional_params)
+
+ Snippet.last
end
context 'when the snippet is spam' do
@@ -163,13 +165,164 @@ describe SnippetsController do
it 'rejects the shippet' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
not_to change { Snippet.count }
- expect(response).to render_template(:new)
end
it 'creates a spam log' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
to change { SpamLog.count }.by(1)
end
+
+ it 'renders :new with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ create_snippet(visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:new)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ create_snippet(visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = create_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet_path(snippet))
+ end
+ end
+ end
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:project) { create :project }
+ let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level }
+
+ def update_snippet(snippet_params = {}, additional_params = {})
+ sign_in(user)
+
+ put :update, {
+ id: snippet.id,
+ personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }.merge(additional_params)
+
+ snippet.reload
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when a private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
+ end
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet_path(snippet))
+ end
+ end
end
end
end
@@ -286,6 +439,24 @@ describe SnippetsController do
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_http_status(200)
end
+
+ context 'CRLF line ending' do
+ let(:personal_snippet) do
+ create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
+ end
+
+ it 'returns LF line endings by default' do
+ get action, id: personal_snippet.to_param
+
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
+
+ it 'does not convert line endings when parameter present' do
+ get action, id: personal_snippet.to_param, line_ending: :raw
+
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
+ end
+ end
end
context 'when not signed in' do
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 570d9fa43f8..f67d26da0ac 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,9 +1,36 @@
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")) }
describe "GET show" do
+ context 'Content-Disposition security measures' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'for PNG files' do
+ it 'returns Content-Disposition: inline' do
+ note = create(:note, :with_attachment, project: project)
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
+ expect(response['Content-Disposition']).to start_with('inline;')
+ end
+ end
+
+ context 'for SVG files' do
+ it 'returns Content-Disposition: attachment' do
+ note = create(:note, :with_svg_attachment, project: project)
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg'
+
+ expect(response['Content-Disposition']).to start_with('attachment;')
+ end
+ end
+ end
+
context "when viewing a user avatar" do
context "when signed in" do
before do
@@ -28,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
@@ -37,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
@@ -54,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
@@ -66,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
@@ -111,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
@@ -135,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
@@ -147,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
@@ -183,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
@@ -212,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
@@ -224,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
@@ -269,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/boards.rb b/spec/factories/boards.rb
index ec46146d9b5..a581725245a 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -3,7 +3,6 @@ FactoryGirl.define do
project factory: :empty_project
after(:create) do |board|
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
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 0397d5d4001..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
@@ -89,8 +104,9 @@ FactoryGirl.define do
tag true
end
- factory :ci_build_with_coverage do
+ trait :coverage do
coverage 99.9
+ coverage_regex '/(d+)/'
end
trait :trace do
@@ -99,6 +115,16 @@ FactoryGirl.define do
end
end
+ trait :erased do
+ erased_at Time.now
+ erased_by factory: :user
+ end
+
+ trait :queued do
+ queued_at Time.now
+ runner factory: :ci_runner
+ end
+
trait :artifacts do
after(:create) do |build, _|
build.artifacts_file =
@@ -128,5 +154,43 @@ FactoryGirl.define do
build.save!
end
end
+
+ trait :with_commit do
+ after(:build) do |build|
+ allow(build).to receive(:commit).and_return build(:commit, :without_author)
+ end
+ end
+
+ trait :with_commit_and_author do
+ after(:build) do |build|
+ 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/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/commits.rb b/spec/factories/commits.rb
index ac6eb0a7897..89e260cf65b 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -8,5 +8,15 @@ FactoryGirl.define do
initialize_with do
new(git_commit, project)
end
+
+ after(:build) do |commit|
+ allow(commit).to receive(:author).and_return build(:author)
+ end
+
+ trait :without_author do
+ after(:build) do |commit|
+ allow(commit).to receive(:author).and_return nil
+ end
+ end
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index bfe41f71b57..55727d6b62c 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -3,6 +3,18 @@ FactoryGirl.define do
project factory: :empty_project
author factory: :user
+ trait(:created) { action Event::CREATED }
+ trait(:updated) { action Event::UPDATED }
+ trait(:closed) { action Event::CLOSED }
+ trait(:reopened) { action Event::REOPENED }
+ trait(:pushed) { action Event::PUSHED }
+ trait(:commented) { action Event::COMMENTED }
+ trait(:merged) { action Event::MERGED }
+ trait(:joined) { action Event::JOINED }
+ trait(:left) { action Event::LEFT }
+ trait(:destroyed) { action Event::DESTROYED }
+ trait(:expired) { action Event::EXPIRED }
+
factory :closed_issue_event do
action { Event::CLOSED }
target factory: :closed_issue
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index d69c5b38d0a..dd93b439b2b 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -2,10 +2,13 @@ FactoryGirl.define do
factory :key do
title
key do
- "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com'
end
factory :deploy_key, class: 'DeployKey' do
+ key do
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O96x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaCrzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy05qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz'
+ end
end
factory :personal_key do
@@ -14,7 +17,7 @@ FactoryGirl.define do
factory :another_key do
key do
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ"
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ'
end
factory :another_deploy_key, class: 'DeployKey' do
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 9e3f06c682c..2a2f3cca91c 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -6,12 +6,6 @@ FactoryGirl.define do
sequence(:position)
end
- factory :backlog_list, parent: :list do
- list_type :backlog
- label nil
- position nil
- end
-
factory :done_list, parent: :list do
list_type :done
label nil
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 22f84150bb3..ae0bbbd6aeb 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -59,8 +59,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 a21da7074f9..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
@@ -97,7 +102,11 @@ FactoryGirl.define do
end
trait :with_attachment do
- attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
+ attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
+ end
+
+ trait :with_svg_attachment do
+ attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") }
end
end
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/pages_domains.rb b/spec/factories/pages_domains.rb
new file mode 100644
index 00000000000..6d2e45f41ba
--- /dev/null
+++ b/spec/factories/pages_domains.rb
@@ -0,0 +1,153 @@
+FactoryGirl.define do
+ factory :pages_domain, class: 'PagesDomain' do
+ domain 'my.domain.com'
+
+ trait :with_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_key do
+ key '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ trait :with_missing_chain do
+ # This certificate is signed with different key
+ # And misses the CA to build trust chain
+ certificate '-----BEGIN CERTIFICATE-----
+MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0
+IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS
+dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9
+ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ
+qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl
+Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A
+/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V
+Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud
+DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4
+QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI
+hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs
+Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h
+s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl
+WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF
+m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+
+VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_trusted_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA'
+ # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority'
+ certificate '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
+ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
+eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow
+gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD
+VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq
+hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw
+AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6
+2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr
+ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt
+4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq
+m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/
+vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT
+8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE
+IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO
+KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO
+GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/
+s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
+JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD
+AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9
+MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy
+bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6
+Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ
+zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj
+Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY
+Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5
+B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx
+PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR
+pu/xO28QOG8=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_expired_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
+cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow
+HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge
+NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS
+NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X
+Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+
+Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9
+ZDXgrA==
+-----END CERTIFICATE-----'
+ end
+ end
+end
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 992580a6b34..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
@@ -56,6 +60,25 @@ FactoryGirl.define do
end
end
+ trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED }
+ trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED }
+ trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE }
+ trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED }
+ trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED }
+ trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE }
+ trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED }
+ trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED }
+ trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE }
+ trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED }
+ trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED }
+ trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
+ trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
+ trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
+ trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
+ trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -106,6 +129,39 @@ FactoryGirl.define do
path { 'gitlabhq' }
test_repo
+
+ transient do
+ create_template nil
+ end
+
+ after :create do |project, evaluator|
+ TestEnv.copy_repo(project)
+
+ if evaluator.create_template
+ args = evaluator.create_template
+
+ project.add_user(args[:user], args[:access])
+
+ project.repository.create_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/bug.md",
+ 'something valid',
+ message: 'test 3',
+ branch_name: 'master')
+ project.repository.create_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/template_test.md",
+ 'template_test',
+ message: 'test 1',
+ branch_name: 'master')
+ project.repository.create_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/feature_proposal.md",
+ 'feature_proposal',
+ message: 'test 2',
+ branch_name: 'master')
+ end
+ end
end
factory :forked_project_with_submodules, parent: :empty_project do
@@ -133,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/timelogs.rb b/spec/factories/timelogs.rb
index 12fc4ec4486..6f1545418eb 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -4,6 +4,6 @@ FactoryGirl.define do
factory :timelog do
time_spent 3600
user
- association :trackable, factory: :issue
+ issue
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 91d6f39a5bf..c1ac3bb84ad 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -14,9 +14,8 @@ FactoryGirl.define do
action { Todo::MENTIONED }
end
- trait :on_commit do
- commit_id RepoHelpers.sample_commit.id
- target_type "Commit"
+ trait :directly_addressed do
+ action { Todo::DIRECTLY_ADDRESSED }
end
trait :build_failed do
@@ -24,6 +23,10 @@ FactoryGirl.define do
target factory: :merge_request
end
+ trait :marked do
+ action { Todo::MARKED }
+ end
+
trait :approval_required do
action { Todo::APPROVAL_REQUIRED }
end
@@ -32,8 +35,21 @@ FactoryGirl.define do
action { Todo::UNMERGEABLE }
end
+ trait :pending do
+ state :pending
+ end
+
trait :done 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 c6f7869516e..249dabbaae8 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -14,10 +14,23 @@ FactoryGirl.define do
admin true
end
+ trait :blocked do
+ after(:build) { |user, _| user.block! }
+ end
+
+ trait :external do
+ external true
+ end
+
trait :two_factor 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/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb
new file mode 100644
index 00000000000..3f3c864ac2b
--- /dev/null
+++ b/spec/factories/wiki_directories.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :wiki_directory do
+ slug '/path_up_to/dir'
+ initialize_with { new(slug) }
+ end
+end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index efa6cbe5bb1..4105f59e289 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -2,8 +2,26 @@ require 'ostruct'
FactoryGirl.define do
factory :wiki_page do
+ transient do
+ attrs do
+ {
+ title: 'Title',
+ content: 'Content for wiki page',
+ format: 'markdown'
+ }
+ end
+ end
+
page { OpenStruct.new(url_path: 'some-name') }
association :wiki, factory: :project_wiki, strategy: :build
initialize_with { new(wiki, page, true) }
+
+ before(:create) do |page, evaluator|
+ page.attributes = evaluator.attrs
+ end
+
+ to_create do |page|
+ page.create
+ end
end
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 7fcfe5a54c7..340884fc986 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
end
end
+
+ describe 'if a many users have been reported for abuse' do
+ let(:report_count) { AbuseReport.default_per_page + 3 }
+
+ before do
+ report_count.times do
+ create(:abuse_report, user: create(:user))
+ end
+ end
+
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
+
+ expect(page).to have_selector('.pagination')
+ expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ end
+ end
+ end
end
end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index e177059d959..9d5ce876c29 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -9,8 +9,8 @@ describe 'Admin Builds' do
let(:pipeline) { create(:ci_pipeline) }
context 'All tab' do
- context 'when have builds' do
- it 'shows all builds' do
+ context 'when have jobs' do
+ it 'shows all jobs' do
create(:ci_build, pipeline: pipeline, status: :pending)
create(:ci_build, pipeline: pipeline, status: :running)
create(:ci_build, pipeline: pipeline, status: :success)
@@ -19,26 +19,26 @@ describe 'Admin Builds' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_selector('.row-content-block', text: 'All builds')
+ expect(page).to have_selector('.row-content-block', text: 'All jobs')
expect(page.all('.build-link').size).to eq(4)
expect(page).to have_link 'Cancel all'
end
end
- context 'when have no builds' do
+ context 'when have no jobs' do
it 'shows a message' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Pending tab' do
- context 'when have pending builds' do
- it 'shows pending builds' do
+ context 'when have pending jobs' do
+ it 'shows pending jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -55,22 +55,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds pending' do
+ context 'when have no jobs pending' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Running tab' do
- context 'when have running builds' do
- it 'shows running builds' do
+ context 'when have running jobs' do
+ it 'shows running jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :running)
build2 = create(:ci_build, pipeline: pipeline, status: :success)
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
@@ -87,22 +87,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds running' do
+ context 'when have no jobs running' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Finished tab' do
- context 'when have finished builds' do
- it 'shows finished builds' do
+ context 'when have finished jobs' do
+ it 'shows finished jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -117,14 +117,14 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds finished' do
+ context 'when have no jobs finished' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel all'
end
end
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_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index eaa42aad0a7..6d6c9165c83 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -35,15 +35,16 @@ RSpec.describe 'admin issues labels' do
it 'deletes all labels', js: true do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
- wait_for_ajax
remove.click
+ wait_for_ajax
end
end
- page.within '.manage-labels-list' do
- expect(page).not_to have_content('bug')
- expect(page).not_to have_content('feature_label')
- end
+ wait_for_ajax
+
+ expect(page).to have_content("There are no labels yet")
+ expect(page).not_to have_content('bug')
+ expect(page).not_to have_content('feature_label')
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index f05fbe3d062..5dcc7d35d82 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -18,7 +18,7 @@ describe "Admin Runners" do
it 'has all necessary texts' do
expect(page).to have_text "To register a new Runner"
- expect(page).to have_text "Runners with last contact less than a minute ago: 1"
+ expect(page).to have_text "Runners with last contact more than a minute ago: 1"
end
describe 'search' do
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/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index a586f8d3184..c0807b8c507 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -211,7 +211,7 @@ describe "Admin::Users", feature: true do
fill_in "user_email", with: "bigbang@mail.com"
fill_in "user_password", with: "AValidPassword1"
fill_in "user_password_confirmation", with: "AValidPassword1"
- check "user_admin"
+ choose "user_access_level_admin"
click_button "Save changes"
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 21ee6cedbae..a7c22615b89 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -23,7 +23,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'])
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 863412d18eb..a01a050a013 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -43,7 +43,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 +54,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
new file mode 100644
index 00000000000..a3e24bb5ffa
--- /dev/null
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -0,0 +1,239 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:label) { create(:label, project: project) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: label, position: 1) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue2) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ context 'modal interaction' do
+ it 'opens modal' do
+ click_button('Add issues')
+
+ expect(page).to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ find('.close').click
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal if cancel button clicked' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ click_button 'Cancel'
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+
+ it 'does not show tooltip on add issues button' do
+ button = page.find('.issue-boards-search button', text: 'Add issues')
+
+ expect(button[:title]).not_to eq("Please add a list to your board first")
+ end
+ end
+
+ context 'issues list' do
+ before do
+ click_button('Add issues')
+
+ wait_for_vue_resource
+ end
+
+ it 'loads issues' do
+ page.within('.add-issues-modal') do
+ page.within('.nav-links') do
+ expect(page).to have_content('2')
+ end
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows selected issues' do
+ page.within('.add-issues-modal') do
+ click_link 'Selected issues'
+
+ expect(page).not_to have_selector('.card')
+ end
+ end
+
+ context 'list dropdown' do
+ it 'resets after deleting list' do
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).to have_button(planning.title)
+
+ click_button 'Cancel'
+ end
+
+ first('.board-delete').click
+
+ click_button('Add issues')
+
+ wait_for_vue_resource
+
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).not_to have_button(planning.title)
+ expect(find('.add-issues-footer')).to have_button(label.title)
+ end
+ end
+ end
+
+ context 'search' do
+ it 'returns issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys(issue.title)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'returns no issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing search')
+
+ expect(page).not_to have_selector('.card')
+ expect(page).not_to have_content("You haven't added any issues to your project yet")
+ end
+ end
+ end
+
+ context 'selecing issues' do
+ it 'selects single issue' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ page.within('.nav-links') do
+ expect(page).to have_content('Selected issues 1')
+ end
+ end
+ end
+
+ it 'changes button text' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
+ end
+ end
+
+ it 'changes button text with plural' do
+ page.within('.add-issues-modal') do
+ all('.card').each do |el|
+ el.click
+ end
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues')
+ end
+ end
+
+ it 'shows only selected issues on selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'selects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'deselects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+
+ click_button 'Deselect all'
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+
+ it 'selects all that arent already selected' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(page).to have_selector('.is-active', count: 1)
+
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'unselects from selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ first('.card').click
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+ end
+
+ context 'adding issues' do
+ it 'adds to board' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+
+ it 'adds to second list' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button planning.title
+
+ click_link label.title
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index bfac5a1b8ab..ecc356f2505 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
+ include DragTo
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
@@ -20,29 +21,35 @@ describe 'Issue Boards', feature: true, js: true do
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'shows blank state' do
expect(page).to have_content('Welcome to your Issue Board!')
end
+ it 'shows tooltip on add issues button' do
+ button = page.find('.issue-boards-search button', text: 'Add issues')
+
+ 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
page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own")
end
- expect(page).to have_selector('.board', count: 2)
+ expect(page).to have_selector('.board', count: 1)
end
it 'creates default lists' do
- lists = ['Backlog', 'To Do', 'Doing', 'Done']
+ lists = ['To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
@@ -64,42 +71,41 @@ 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(:issue, :confidential, project: project, author: user) }
- let!(:issue1) { create(:issue, project: project, assignee: user) }
- let!(:issue2) { create(:issue, project: project, author: user2) }
- let!(:issue3) { create(:issue, project: project) }
- let!(:issue4) { create(:issue, project: project) }
- 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: [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)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
expect(find('.board:nth-child(1)')).to have_selector('.card')
expect(find('.board:nth-child(2)')).to have_selector('.card')
expect(find('.board:nth-child(3)')).to have_selector('.card')
- expect(find('.board:nth-child(4)')).to have_selector('.card')
end
it 'shows lists' do
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
end
it 'shows description tooltip on list title' do
- page.within('.board:nth-child(2)') do
+ page.within('.board:nth-child(1)') do
expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end
end
it 'shows issues in lists' do
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
@@ -108,19 +114,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'search backlog list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue1.title)
- end
-
- wait_for_vue_resource
-
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
- end
-
it 'search done list' do
page.within('#js-boards-search') do
find('.form-control').set(issue8.title)
@@ -130,8 +123,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
end
it 'search list' do
@@ -141,157 +133,135 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'allows user to delete board' do
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list'
wait_for_ajax
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
- expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ expect(page).to have_selector('.board', count: 2)
end
it 'infinite scrolls list' do
50.times do
- create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
end
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('56')
+ expect(page.find('.board-header')).to have_content('58')
expect(page).to have_selector('.card', count: 20)
- expect(page).to have_content('Showing 20 of 56 issues')
+ expect(page).to have_content('Showing 20 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
- expect(page).to have_content('Showing 40 of 56 issues')
+ expect(page).to have_content('Showing 40 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
- expect(page).to have_selector('.card', count: 56)
+ expect(page).to have_selector('.card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
- context 'backlog' do
- it 'shows issues in backlog with no labels' do
- wait_for_board_cards(1, 6)
- end
-
- it 'moves issue from backlog into list' do
- drag_to(list_to_index: 1)
-
- wait_for_vue_resource
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 3)
- end
- end
-
context 'done' do
it 'shows list of done issues' do
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(3, 1)
wait_for_ajax
end
it 'moves issue to done' do
- drag_to(list_from_index: 0, list_to_index: 3)
+ drag(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 5)
+ wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 2)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
- expect(find('.board:nth-child(4)')).to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
it 'removes all of the same issue to done' do
- drag_to(list_from_index: 1, list_to_index: 3)
+ drag(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 2)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
- expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
end
context 'lists' do
it 'changes position of list' do
- drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+ drag(list_from_index: 1, list_to_index: 0, selector: '.board-header')
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 2)
+ wait_for_board_cards(2, 8)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(2)')).to have_content(development.title)
- expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(development.title)
+ expect(find('.board:nth-child(1)')).to have_content(planning.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+ drag(list_from_index: 0, from_index: 1, list_to_index: 1)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 3)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 2, list_to_index: 1)
+ drag(list_from_index: 1, list_to_index: 0)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 3)
+ wait_for_board_cards(1, 9)
+ wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 1)
- expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)
end
it 'issue moves from done' do
- drag_to(list_from_index: 3, list_to_index: 1)
+ drag(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
- wait_for_board_cards(1, 6)
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 3)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 0)
+ wait_for_board_cards(3, 0)
end
context 'issue card' do
@@ -324,7 +294,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Backlog label' do
@@ -337,7 +307,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Done label' do
@@ -350,7 +320,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'keeps dropdown open after adding new list' do
@@ -366,21 +336,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.issue-boards-search')).to have_selector('.open')
end
- it 'moves issues from backlog into new list' do
- wait_for_board_cards(1, 6)
-
- click_button 'Add list'
- wait_for_ajax
-
- page.within('.dropdown-menu-issues-board-new') do
- click_link testing.title
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- end
-
it 'creates new list from a new label' do
click_button 'Add list'
@@ -397,7 +352,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
end
end
@@ -418,7 +373,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by assignee' do
@@ -437,7 +392,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by milestone' do
@@ -454,10 +409,9 @@ describe 'Issue Boards', feature: true, js: true do
end
wait_for_vue_resource
- wait_for_board_cards(1, 0)
- wait_for_board_cards(2, 1)
+ wait_for_board_cards(1, 1)
+ wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 0)
end
it 'filters by label' do
@@ -474,7 +428,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by label with space after reload' do
@@ -530,7 +484,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'infinite scrolls list with label filter' do
50.times do
- create(:labeled_issue, project: project, labels: [testing])
+ create(:labeled_issue, project: project, labels: [planning, testing])
end
page.within '.issues-filters' do
@@ -580,32 +534,12 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
- end
-
- it 'filters by no label' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link("No Label")
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 0)
- wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 1)
+ wait_for_empty_boards((2..3))
end
it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do
- expect(page).to have_selector('.card', count: 6)
+ expect(page).to have_selector('.card', count: 8)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_vue_resource
@@ -614,7 +548,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
@@ -688,14 +622,13 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
- evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
-
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
- end
-
- wait_for_vue_resource
+ 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
def wait_for_board_cards(board_number, expected_cards)
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
new file mode 100644
index 00000000000..1cf0d11d448
--- /dev/null
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -0,0 +1,259 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal filtering', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:issue1) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
+ it 'shows empty state when no results found' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing empty state')
+
+ wait_for_vue_resource
+
+ expect(page).to have_content('There are no issues to show.')
+ end
+ end
+
+ it 'restores filters when closing' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Upcoming'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 0)
+
+ click_button 'Cancel'
+ end
+
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ context 'author' do
+ let!(:issue) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ visit_board
+ end
+
+ it 'filters by any author' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link 'Any Author'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by selected user' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link user2.name
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'assignee' do
+ let!(:issue) { create(:issue, project: project, assignee: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ 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'
+
+ wait_for_vue_resource
+
+ 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
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+
+ before 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'
+
+ wait_for_vue_resource
+
+ 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
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'label' do
+ let(:label) { create(:label, project: project) }
+ let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before 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'
+
+ wait_for_vue_resource
+
+ 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
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ def visit_board
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ click_button('Add issues')
+ end
+end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index a03cd6fbf2d..6d14a8cf483 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) }
context 'authorized user' do
@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'displays new issue button' do
@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it 'does not display new issue button in done list' do
- page.within('.board:nth-child(3)') do
+ page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c28bb0dcdae..3332e07ec31 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -4,16 +4,22 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:empty_project, :public) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let!(:label) { create(:label, project: project) }
- let!(:label2) { create(:label, project: project) }
- let!(:milestone) { create(:milestone, project: project) }
- let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
- let!(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:development) { create(:label, project: project, name: 'Development') }
+ 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], 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') }
before do
+ Timecop.freeze
+
project.team << [user, :master]
login_as(user)
@@ -22,56 +28,62 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ after do
+ Timecop.return
+ end
+
it 'shows sidebar when clicking issue' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
end
it 'closes sidebar when clicking issue' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).not_to have_selector('.issue-boards-sidebar')
end
it 'closes sidebar when clicking close button' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
- find('.gutter-toggle').click
+ find('.gutter-toggle').trigger('click')
expect(page).not_to have_selector('.issue-boards-sidebar')
end
it 'shows issue details when sidebar is open' do
- page.within(first('.board')) do
- first('.card').click
+ click_card(card)
+
+ page.within('.issue-boards-sidebar') do
+ expect(page).to have_content(issue2.title)
+ expect(page).to have_content(issue2.to_reference)
end
+ end
+
+ it 'removes card from board when clicking ' do
+ click_card(card)
page.within('.issue-boards-sidebar') do
- expect(page).to have_content(issue.title)
- expect(page).to have_content(issue.to_reference)
+ click_button 'Remove from board'
+ end
+
+ wait_for_vue_resource
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card', count: 1)
end
end
context 'assignee' do
it 'updates the issues assignee' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.assignee') do
click_link 'Edit'
@@ -87,17 +99,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content(user.name)
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.avatar')
- end
- end
+ expect(card).to have_selector('.avatar')
end
it 'removes the assignee' do
- page.within(first('.board')) do
- find('.card:nth-child(2)').click
- end
+ card_two = first('.board').find('.card:nth-child(2)')
+ click_card(card_two)
page.within('.assignee') do
click_link 'Edit'
@@ -113,17 +120,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('No assignee')
end
- page.within(first('.board')) do
- page.within(find('.card:nth-child(2)')) do
- expect(page).not_to have_selector('.avatar')
- end
- end
+ expect(card_two).not_to have_selector('.avatar')
end
it 'assignees to current user' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
@@ -135,17 +136,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content(user.name)
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.avatar')
- end
- end
+ expect(card).to have_selector('.avatar')
end
it 'resets assignee dropdown' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.assignee') do
click_link 'Edit'
@@ -175,9 +170,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'milestone' do
it 'adds a milestone' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.milestone') do
click_link 'Edit'
@@ -195,9 +188,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'removes a milestone' do
- page.within(first('.board')) do
- find('.card:nth-child(2)').click
- end
+ click_card(card)
page.within('.milestone') do
click_link 'Edit'
@@ -217,14 +208,12 @@ describe 'Issue Boards', feature: true, js: true do
context 'due date' do
it 'updates due date' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.due_date') do
click_link 'Edit'
- click_link Date.today.day
+ click_button Date.today.day
wait_for_vue_resource
@@ -235,104 +224,84 @@ describe 'Issue Boards', feature: true, js: true do
context 'labels' do
it 'adds a single label' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_ajax
- click_link label.title
+ click_link bug.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
+ expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_content(bug.title)
end
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
- end
- end
+ expect(card).to have_selector('.label', count: 2)
+ expect(card).to have_content(bug.title)
end
it 'adds a multiple labels' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_ajax
- click_link label.title
- click_link label2.title
+ click_link bug.title
+ click_link regression.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
+ expect(page).to have_selector('.label', count: 4)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
end
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
- end
- end
+ expect(card).to have_selector('.label', count: 3)
+ expect(card).to have_content(bug.title)
+ expect(card).to have_content(regression.title)
end
it 'removes a label' do
- page.within(first('.board')) do
- find('.card:nth-child(2)').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_ajax
- click_link label.title
+ click_link stretch.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 0)
- expect(page).not_to have_content(label.title)
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).not_to have_content(stretch.title)
end
end
- page.within(first('.board')) do
- page.within(find('.card:nth-child(2)')) do
- expect(page).not_to have_selector('.label', count: 1)
- expect(page).not_to have_content(label.title)
- end
- end
+ expect(card).not_to have_selector('.label')
+ expect(card).not_to have_content(stretch.title)
end
end
context 'subscription' do
it 'changes issue subscription' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.subscription') do
click_button 'Subscribe'
@@ -341,4 +310,19 @@ describe 'Issue Boards', feature: true, js: true do
end
end
end
+
+ def click_card(card)
+ page.within(card) do
+ first('.card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 3e0b6364e0d..35d090c4b7f 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
-feature 'Contributions Calendar', js: true, feature: true do
+feature 'Contributions Calendar', :feature, :js do
include WaitForAjax
+ let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public) }
+ let(:issue_note) { create(:note, project: contributed_project) }
# Ex/ Sunday Jan 1, 2016
date_format = '%A %b %-d, %Y'
@@ -12,31 +14,31 @@ feature 'Contributions Calendar', js: true, feature: true do
issue_params = { title: issue_title }
def get_cell_color_selector(contributions)
- contribution_cell = '.user-contrib-cell'
- activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77']
- activity_colors_index = 0
-
- if contributions > 0 && contributions < 10
- activity_colors_index = 1
- elsif contributions >= 10 && contributions < 20
- activity_colors_index = 2
- elsif contributions >= 20 && contributions < 30
- activity_colors_index = 3
- elsif contributions >= 30
- activity_colors_index = 4
- end
+ activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77]
+ # We currently don't actually test the cases with contributions >= 20
+ activity_colors_index =
+ if contributions > 0 && contributions < 10
+ 1
+ elsif contributions >= 10 && contributions < 20
+ 2
+ elsif contributions >= 20 && contributions < 30
+ 3
+ elsif contributions >= 30
+ 4
+ else
+ 0
+ end
- "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']"
+ ".user-contrib-cell[fill='#{activity_colors[activity_colors_index]}']"
end
def get_cell_date_selector(contributions, date)
- contribution_text = 'No contributions'
-
- if contributions === 1
- contribution_text = '1 contribution'
- elsif contributions > 1
- contribution_text = "#{contributions} contributions"
- end
+ contribution_text =
+ if contributions.zero?
+ 'No contributions'
+ else
+ "#{contributions} #{'contribution'.pluralize(contributions)}"
+ end
"#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']"
end
@@ -45,129 +47,155 @@ feature 'Contributions Calendar', js: true, feature: true do
push_params = {
project: contributed_project,
action: Event::PUSHED,
- author_id: @user.id,
+ author_id: user.id,
data: { commit_count: 3 }
}
Event.create(push_params)
end
- def get_first_cell_content
- find('.user-calendar-activities').text
- end
+ def note_comment_contribution
+ note_comment_params = {
+ project: contributed_project,
+ action: Event::COMMENTED,
+ target: issue_note,
+ author_id: user.id
+ }
- before do
- login_as :user
- visit @user.username
- wait_for_ajax
+ Event.create(note_comment_params)
end
- it 'displays calendar', js: true do
- expect(page).to have_css('.js-contrib-calendar')
+ def selected_day_activities
+ find('.user-calendar-activities').text
end
- describe 'select calendar day', js: true do
- let(:cells) { page.all('.user-contrib-cell') }
- let(:first_cell_content_before) { get_first_cell_content }
+ before do
+ login_as user
+ end
+ describe 'calendar day selection' do
before do
- cells[0].click
+ visit user.username
wait_for_ajax
- first_cell_content_before
end
- it 'displays calendar day activities', js: true do
- expect(get_first_cell_content).not_to eq('')
+ it 'displays calendar' do
+ expect(page).to have_css('.js-contrib-calendar')
end
- describe 'select another calendar day', js: true do
+ describe 'select calendar day' do
+ let(:cells) { page.all('.user-contrib-cell') }
+
before do
- cells[1].click
+ cells[0].click
wait_for_ajax
+ @first_day_activities = selected_day_activities
end
- it 'displays different calendar day activities', js: true do
- expect(get_first_cell_content).not_to eq(first_cell_content_before)
+ it 'displays calendar day activities' do
+ expect(selected_day_activities).not_to be_empty
end
- end
- describe 'deselect calendar day', js: true do
- before do
- cells[0].click
- wait_for_ajax
+ describe 'select another calendar day' do
+ before do
+ cells[1].click
+ wait_for_ajax
+ end
+
+ it 'displays different calendar day activities' do
+ expect(selected_day_activities).not_to eq(@first_day_activities)
+ end
end
- it 'hides calendar day activities', js: true do
- expect(get_first_cell_content).to eq('')
+ describe 'deselect calendar day' do
+ before do
+ cells[0].click
+ wait_for_ajax
+ end
+
+ it 'hides calendar day activities' do
+ expect(selected_day_activities).to be_empty
+ end
end
end
end
- describe '1 calendar activity' do
- before do
- Issues::CreateService.new(contributed_project, @user, issue_params).execute
- visit @user.username
- wait_for_ajax
+ describe 'calendar daily activities' do
+ shared_context 'visit user page' do
+ before do
+ visit user.username
+ wait_for_ajax
+ end
end
- it 'displays calendar activity log', js: true do
- expect(find('.content_list .event-note')).to have_content issue_title
- end
+ shared_examples 'a day with activity' do |contribution_count:|
+ include_context 'visit user page'
- it 'displays calendar activity square color for 1 contribution', js: true do
- expect(page).to have_selector(get_cell_color_selector(1), count: 1)
- end
+ it 'displays calendar activity square color for 1 contribution' do
+ expect(page).to have_selector(get_cell_color_selector(contribution_count), count: 1)
+ end
- it 'displays calendar activity square on the correct date', js: true do
- today = Date.today.strftime(date_format)
- expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ it 'displays calendar activity square on the correct date' do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ end
end
- end
- describe '10 calendar activities' do
- before do
- (0..9).each do |i|
- push_code_contribution()
+ describe '1 issue creation calendar activity' do
+ before do
+ Issues::CreateService.new(contributed_project, user, issue_params).execute
end
- visit @user.username
- wait_for_ajax
- end
+ it_behaves_like 'a day with activity', contribution_count: 1
- it 'displays calendar activity square color for 10 contributions', js: true do
- expect(page).to have_selector(get_cell_color_selector(10), count: 1)
- end
+ describe 'issue title is shown on activity page' do
+ include_context 'visit user page'
- it 'displays calendar activity square on the correct date', js: true do
- today = Date.today.strftime(date_format)
- expect(page).to have_selector(get_cell_date_selector(10, today), count: 1)
+ it 'displays calendar activity log' do
+ expect(find('.content_list .event-note')).to have_content issue_title
+ end
+ end
end
- end
- describe 'calendar activity on two days' do
- before do
- push_code_contribution()
-
- Timecop.freeze(Date.yesterday)
- Issues::CreateService.new(contributed_project, @user, issue_params).execute
- Timecop.return
+ describe '1 comment calendar activity' do
+ before do
+ note_comment_contribution
+ end
- visit @user.username
- wait_for_ajax
+ it_behaves_like 'a day with activity', contribution_count: 1
end
- it 'displays calendar activity squares for both days', js: true do
- expect(page).to have_selector(get_cell_color_selector(1), count: 2)
- end
+ describe '10 calendar activities' do
+ before do
+ 10.times { push_code_contribution }
+ end
- it 'displays calendar activity square for yesterday', js: true do
- yesterday = Date.yesterday.strftime(date_format)
- expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ it_behaves_like 'a day with activity', contribution_count: 10
end
- it 'displays calendar activity square for today', js: true do
- today = Date.today.strftime(date_format)
- expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution
+
+ Timecop.freeze(Date.yesterday) do
+ Issues::CreateService.new(contributed_project, user, issue_params).execute
+ end
+ end
+ include_context 'visit user page'
+
+ it 'displays calendar activity squares for both days' do
+ expect(page).to have_selector(get_cell_color_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday' do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today' do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 8f561c8f90b..0e305c52358 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -153,7 +153,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name
expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
- expect(page).not_to have_link('Retry failed')
+ expect(page).not_to have_link('Retry')
end
end
@@ -172,7 +172,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name
expect(page).not_to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
- expect(page).not_to have_link('Retry failed')
+ expect(page).not_to have_link('Retry')
end
end
end
@@ -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 f3a5b565122..4638812b2d9 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -251,6 +251,8 @@ describe 'Copy as GFM', feature: true, js: true do
'SanitizationFilter',
<<-GFM.strip_heredoc
+ <a name="named-anchor"></a>
+
<sub>sub</sub>
<dl>
@@ -273,6 +275,10 @@ describe 'Copy as GFM', feature: true, js: true do
<rp>rp</rp>
<abbr>abbr</abbr>
+
+ <summary>summary</summary>
+
+ <details>details</details>
GFM
)
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 7d59fcac517..ae750be4d4a 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -1,14 +1,15 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Active Tab', feature: true do
+RSpec.describe 'Dashboard Active Tab', js: true, feature: true do
before do
login_as :user
end
shared_examples 'page has active tab' do |title|
it "#{title} tab" do
- expect(page).to have_selector('.nav-sidebar li.active', count: 1)
- expect(find('.nav-sidebar li.active')).to have_content(title)
+ find('.global-dropdown-toggle').trigger('click')
+ expect(page).to have_selector('.global-dropdown-menu li.active', count: 1)
+ expect(find('.global-dropdown-menu li.active')).to have_content(title)
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/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 41dcfe439c2..a1718912fc6 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -35,8 +35,9 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
end
def expect_counters(issuable_type, count)
- dashboard_count = find('li.active span.badge')
- nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count")
+ dashboard_count = find('li.active')
+ find('.global-dropdown-toggle').click
+ nav_count = find(".dashboard-shortcuts-#{issuable_type}")
expect(nav_count).to have_content(count)
expect(dashboard_count).to have_content(count)
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
new file mode 100644
index 00000000000..f4420814c3a
--- /dev/null
+++ b/spec/features/dashboard/issues_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Issues', feature: true do
+ let(:current_user) { create :user }
+ let(:public_project) { create(:empty_project, :public) }
+ let(:project) do
+ create(:empty_project) do |project|
+ project.team << [current_user, :master]
+ end
+ end
+
+ let!(:authored_issue) { create :issue, author: current_user, project: project }
+ let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
+ let!(:assigned_issue) { create :issue, assignee: current_user, project: project }
+ let!(:other_issue) { create :issue, project: project }
+
+ before do
+ login_as(current_user)
+
+ visit issues_dashboard_path(assignee_id: current_user.id)
+ end
+
+ it 'shows issues assigned to current user' do
+ expect(page).to have_content(assigned_issue.title)
+ expect(page).not_to have_content(authored_issue.title)
+ expect(page).not_to have_content(other_issue.title)
+ end
+
+ it 'shows issues when current user is author', js: true do
+ find('#assignee_id', visible: false).set('')
+ find('.js-author-search', match: :first).click
+ find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
+
+ expect(page).to have_content(authored_issue.title)
+ expect(page).to have_content(authored_issue_on_public_project.title)
+ expect(page).not_to have_content(assigned_issue.title)
+ expect(page).not_to have_content(other_issue.title)
+ end
+
+ it 'shows all issues' do
+ click_link('Reset filters')
+
+ expect(page).to have_content(authored_issue.title)
+ expect(page).to have_content(authored_issue_on_public_project.title)
+ 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..63eb5c697c2
--- /dev/null
+++ b/spec/features/dashboard/projects_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Projects', feature: true do
+ before do
+ login_as(create :user)
+ visit dashboard_projects_path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index d9be4e5dbdd..62a2c54c94c 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do
find('body').native.send_key('g')
find('body').native.send_key('p')
- ensure_active_main_tab('Projects')
+ check_page_title('Projects')
find('body').native.send_key('g')
find('body').native.send_key('i')
- ensure_active_main_tab('Issues')
+ check_page_title('Issues')
find('body').native.send_key('g')
find('body').native.send_key('m')
- ensure_active_main_tab('Merge Requests')
+ check_page_title('Merge Requests')
end
- def ensure_active_main_tab(content)
- expect(find('.nav-sidebar li.active')).to have_content(content)
+ def check_page_title(title)
+ expect(find('.header-content .title')).to have_content(title)
end
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 511c95b758f..00000000000
--- a/spec/features/environment_spec.rb
+++ /dev/null
@@ -1,197 +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 stop button' do
- expect(page).not_to have_link('Stop')
- 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
- 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 '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
- 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 c033b693213..00000000000
--- a/spec/features/environments_spec.rb
+++ /dev/null
@@ -1,229 +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
- 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/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8b3e2fa93a2..8c64b050e19 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -72,8 +72,8 @@ feature 'Expand and collapse diffs', js: true, feature: true do
it 'collapses large diffs for renamed files by default' do
expect(large_diff_renamed).not_to have_selector('.code')
expect(large_diff_renamed).to have_selector('.nothing-here-block')
- expect(large_diff_renamed).to have_selector('.file-title .deletion')
- expect(large_diff_renamed).to have_selector('.file-title .addition')
+ expect(large_diff_renamed).to have_selector('.js-file-title .deletion')
+ expect(large_diff_renamed).to have_selector('.js-file-title .addition')
end
it 'shows non-renderable diffs as such immediately, regardless of their size' do
@@ -115,9 +115,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a large diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `large_diff.md` title
- all('.file-title')[1].click
+ all('.diff-toggle-caret')[1].click
wait_for_ajax
end
@@ -159,9 +159,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding the diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `large_diff.md` title
- all('.file-title')[1].click
+ all('.diff-toggle-caret')[1].click
wait_for_ajax
end
@@ -181,9 +181,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'collapsing an expanded diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'hides the diff content' do
@@ -194,9 +194,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 're-expanding the same diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'shows the diff content' do
@@ -290,9 +290,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'collapsing an expanded diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'hides the diff content' do
@@ -303,9 +303,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 're-expanding the same diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'shows the diff content' do
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/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/members/list_spec.rb b/spec/features/groups/members/list_spec.rb
new file mode 100644
index 00000000000..14c193f7450
--- /dev/null
+++ b/spec/features/groups/members/list_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Groups members list', feature: true do
+ let(:user1) { create(:user, name: 'John Doe') }
+ let(:user2) { create(:user, name: 'Mary Jane') }
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ background do
+ login_as(user1)
+ end
+
+ scenario 'show members from current group and parent' do
+ group.add_developer(user1)
+ nested_group.add_developer(user2)
+
+ visit group_group_members_path(nested_group)
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row.text).to include(user2.name)
+ end
+
+ scenario 'show user once if member of both current group and parent' do
+ group.add_developer(user1)
+ nested_group.add_developer(user1)
+
+ visit group_group_members_path(nested_group)
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row).to be_blank
+ end
+
+ it 'updates user to owner level', :js do
+ group.add_owner(user1)
+ group.add_developer(user2)
+
+ visit group_group_members_path(group)
+
+ page.within(second_row) do
+ click_button('Developer')
+
+ click_link('Owner')
+
+ expect(page).to have_button('Owner')
+ end
+ end
+
+ def first_row
+ page.all('ul.content-list > li')[0]
+ end
+
+ def second_row
+ page.all('ul.content-list > li')[1]
+ end
+end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 78a11ffee99..b55078c3bf6 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
- let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) }
+ let(:project_archived) { create(:project, :archived, :merge_requests_enabled, group: group) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
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 a515c92db37..d243f9478bb 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -43,6 +43,61 @@ 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
+ let(:group) { create(:group, path: 'foo') }
+
+ before do
+ visit subgroups_group_path(group)
+ click_link 'New Subgroup'
+ end
+
+ it 'creates a nested group' do
+ fill_in 'Group path', with: 'bar'
+ click_button 'Create group'
+
+ expect(current_path).to eq(group_path('foo/bar'))
+ expect(page).to have_content("Group 'bar' was successfully created.")
+ end
end
describe 'group edit' do
@@ -88,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
@@ -117,7 +172,7 @@ feature 'Group', feature: true do
visit path
click_link 'Subgroups'
- expect(page).to have_content(nested_group.full_name)
+ expect(page).to have_content(nested_group.name)
end
end
end
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/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
new file mode 100644
index 00000000000..b90bf6268fd
--- /dev/null
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -0,0 +1,75 @@
+require 'rails_helper'
+
+describe 'issuable list', feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ issuable_types = [:issue, :merge_request]
+
+ before do
+ project.add_user(user, :developer)
+ login_as(user)
+ issuable_types.each { |type| create_issuables(type) }
+ end
+
+ issuable_types.each do |issuable_type|
+ it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do
+ control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count
+
+ create_issuables(issuable_type)
+
+ expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count)
+ end
+
+ it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do
+ visit_issuable_list(issuable_type)
+
+ expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1)
+ expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1)
+ expect(first('.fa-comments').find(:xpath, '..')).to have_content(2)
+ 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)
+ else
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+ end
+
+ def create_issuables(issuable_type)
+ 3.times do
+ 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')
+ end
+
+ 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..f424186cf30 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do
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 +44,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 +67,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 +92,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 +113,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..19a00618b12 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) }
@@ -121,14 +122,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 5079eb8dd00..b192064b693 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,39 +1,43 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do
- include WaitForAjax
-
- let!(:project) { create(:empty_project) }
- let!(:user) { create(:user) }
- let!(:bug_label) { create(:label, project: project, title: 'bug') }
- let!(:uppercase_label) { create(:label, project: project, title: 'BUG') }
- let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
- let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
- let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
- let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')}
- let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')}
+ include FilteredSearchHelpers
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_label) { '#js-dropdown-label' }
+ let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") }
+
+ shared_context 'with labels' do
+ let!(:bug_label) { create(:label, project: project, title: 'bug-label') }
+ let!(:uppercase_label) { create(:label, project: project, title: 'BUG-LABEL') }
+ let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
+ let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
+ let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
+ let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()') }
+ let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
+ end
- 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
+ def search_for_label(label)
+ init_label_search
+ filtered_search.send_keys(label)
+ end
+
+ def click_label(text)
+ filter_dropdown.find('.filter-dropdown-item', text: text).click
end
def dropdown_label_size
- page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size
+ filter_dropdown.all('.filter-dropdown-item').size
end
- def click_label(text)
- find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
end
before do
- project.team << [user, :master]
+ project.add_master(user)
login_as(user)
create(:issue, project: project)
@@ -42,11 +46,13 @@ describe 'Dropdown label', js: true, feature: true do
describe 'keyboard navigation' do
it 'selects label' do
- send_keys_to_filtered_search('label:')
+ bug_label = create(:label, project: project, title: 'bug-label')
+ init_label_search
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq("label:~#{special_label.name} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
end
@@ -54,216 +60,233 @@ describe 'Dropdown label', js: true, feature: true do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+ expect(page).to have_css(js_dropdown_label)
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened and hides it when loaded' do
filtered_search.set('label:')
- expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true)
- end
-
- it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('label:')
-
- expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading')
+ expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the labels when opened' do
- send_keys_to_filtered_search('label:')
+ it 'loads all the labels when opened' do
+ bug_label = create(:label, project: project, title: 'bug-label')
+ filtered_search.set('label:')
- expect(dropdown_label_size).to be > 0
+ expect(filter_dropdown).to have_content(bug_label.title)
+ expect(dropdown_label_size).to eq(1)
end
end
describe 'filtering' do
- before do
- filtered_search.set('label')
- end
-
- it 'filters by name' do
- send_keys_to_filtered_search(':b')
+ include_context 'with labels'
- expect(dropdown_label_size).to eq(2)
+ before do
+ init_label_search
end
- it 'filters by case insensitive name' do
- send_keys_to_filtered_search(':B')
+ it 'filters by case-insensitive name with or without symbol' do
+ 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
expect(dropdown_label_size).to eq(2)
- end
-
- it 'filters by name with symbol' do
- send_keys_to_filtered_search(':~bu')
- expect(dropdown_label_size).to eq(2)
- end
+ clear_search_field
+ init_label_search
- it 'filters by case insensitive name with symbol' do
- send_keys_to_filtered_search(':~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
expect(dropdown_label_size).to eq(2)
end
- it 'filters by multiple words' do
- send_keys_to_filtered_search(':Hig')
+ it 'filters by multiple words with or without symbol' do
+ filtered_search.send_keys('Hig')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words with symbol' do
- send_keys_to_filtered_search(':~Hig')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~Hig')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing single quotes' do
- send_keys_to_filtered_search(':won\'t')
+ it 'filters by multiple words containing single quotes with or without symbol' do
+ filtered_search.send_keys('won\'t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing single quotes with symbol' do
- send_keys_to_filtered_search(':~won\'t')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~won\'t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing double quotes' do
- send_keys_to_filtered_search(':won"t')
+ it 'filters by multiple words containing double quotes with or without symbol' do
+ filtered_search.send_keys('won"t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing double quotes with symbol' do
- send_keys_to_filtered_search(':~won"t')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~won"t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by special characters' do
- send_keys_to_filtered_search(':^+')
+ it 'filters by special characters with or without symbol' do
+ filtered_search.send_keys('^+')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by special characters with symbol' do
- send_keys_to_filtered_search(':~^+')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~^+')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
end
describe 'selecting from dropdown' do
+ include_context 'with labels'
+
before do
- filtered_search.set('label:')
+ init_label_search
end
it 'fills in the label name when the label has not been filled' do
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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
- send_keys_to_filtered_search('bu')
+ filtered_search.send_keys('bu')
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{special_label.title} ")
+ expect(page).not_to have_css(js_dropdown_label)
+ 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
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
- expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:none ")
+ expect(page).not_to have_css(js_dropdown_label)
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
end
describe 'input has existing content' do
it 'opens label dropdown with existing search term' do
filtered_search.set('searchTerm label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing author' do
filtered_search.set('author:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing assignee' do
filtered_search.set('assignee:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing label' do
filtered_search.set('label:~urgent label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing milestone' do
filtered_search.set('milestone:%v2.0 label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
- initial_size = dropdown_label_size
+ create(:label, project: project, title: 'bug-label')
+ init_label_search
- expect(initial_size).to be > 0
+ expect(dropdown_label_size).to eq(1)
create(:label, project: project)
- find('.filtered-search-input-container .clear-search').click
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
+ clear_search_field
+ init_label_search
- expect(dropdown_label_size).to eq(initial_size)
+ expect(dropdown_label_size).to eq(1)
end
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..85ffffe4b6d 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,86 @@ 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
end
@@ -222,16 +238,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 3f70a6aa75f..f079a9627e4 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,6 +1,7 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:group) { create(:group) }
@@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do
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) }
- let(:filtered_search) { find('.filtered-search') }
-
- def input_filtered_search(search_term, submit: true)
- filtered_search.set(search_term)
-
- if submit
- filtered_search.send_keys(:enter)
- end
- end
-
- def expect_filtered_search_input(input)
- expect(find('.filtered-search').value).to eq(input)
- end
def expect_no_issues_list
page.within '.issues-list' do
@@ -109,160 +97,188 @@ 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
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
it 'filters issues by multiple authors' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
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
it 'sorting' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
end
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
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
it 'filters issues by multiple assignees' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
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
context 'sorting' do
it 'sorts' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
end
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
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
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
@@ -270,21 +286,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
@@ -294,29 +309,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
@@ -324,11 +339,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
@@ -336,86 +351,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
@@ -423,14 +467,14 @@ 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
context 'sorting' do
it 'sorts' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
end
end
@@ -440,133 +484,112 @@ 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 invalid milestones' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
it 'filters issues by multiple milestones' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
it 'filters issues by milestone containing special characters' 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
context 'sorting' do
it 'sorts' do
- pending('to be tested, issue #26546')
- expect(true).to be(false)
+ skip('to be tested, issue #26546')
end
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
@@ -628,80 +651,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
@@ -740,8 +764,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
@@ -801,4 +825,26 @@ describe 'Filter issues', js: true, feature: true do
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
end
+
+ context 'URL has a trailing slash' do
+ before do
+ visit "#{namespace_project_issues_path(project.namespace, project)}/"
+ end
+
+ it 'milestone dropdown loads milestones' do
+ input_filtered_search("milestone:", submit: false)
+
+ within('#js-dropdown-milestone') do
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 2)
+ end
+ end
+
+ it 'label dropdown load labels' do
+ input_filtered_search("label:", submit: false)
+
+ within('#js-dropdown-label') do
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
+ end
+ end
+ end
end
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/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 93139dc9e94..7135565294b 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).not_to have_selector('.atwho-view')
end
+ it 'triggers autocomplete after selecting a slash command' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys('/as')
+ note.click
+ end
+
+ find('.atwho-view li', text: '/assign').native.send_keys(:tab)
+
+ user_item = find('.atwho-view li', text: user.username)
+ expect(user_item).to have_content(user.username)
+ end
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 1eb981942ea..7b9d4534ada 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -7,9 +7,9 @@ feature 'Issue Sidebar', feature: true do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
+ let!(:label) { create(:label, project: project, title: 'bug') }
before do
- create(:label, project: project, title: 'bug')
login_as(user)
end
@@ -50,16 +50,6 @@ feature 'Issue Sidebar', feature: true do
visit_issue(project, issue)
end
- describe 'when clicking on edit labels', js: true do
- it 'shows dropdown option to create a new label' do
- find('.block.labels .edit-link').click
-
- page.within('.block.labels') do
- expect(page).to have_content 'Create new'
- end
- end
- end
-
context 'sidebar', js: true do
it 'changes size when the screen size is smaller' do
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
@@ -77,36 +67,53 @@ feature 'Issue Sidebar', feature: true do
end
end
- context 'creating a new label', js: true do
- it 'shows option to crate a new label is present' do
+ context 'editing issue labels', js: true do
+ before do
page.within('.block.labels') do
find('.edit-link').click
+ end
+ end
+ it 'shows option to create a new label' do
+ page.within('.block.labels') do
expect(page).to have_content 'Create new'
end
end
- it 'shows dropdown switches to "create label" section' do
- page.within('.block.labels') do
- find('.edit-link').click
- click_link 'Create new'
+ context 'creating a new label', js: true do
+ before do
+ page.within('.block.labels') do
+ click_link 'Create new'
+ end
+ end
- expect(page).to have_content 'Create new label'
+ it 'shows dropdown switches to "create label" section' do
+ page.within('.block.labels') do
+ expect(page).to have_content 'Create new label'
+ end
end
- end
- it 'adds new label' do
- page.within('.block.labels') do
- find('.edit-link').click
- sleep 1
- click_link 'Create new'
+ it 'adds new label' do
+ page.within('.block.labels') do
+ fill_in 'new_label_name', with: 'wontfix'
+ page.find(".suggest-colors a", match: :first).click
+ click_button 'Create'
+
+ page.within('.dropdown-page-one') do
+ expect(page).to have_content 'wontfix'
+ end
+ end
+ end
- fill_in 'new_label_name', with: 'wontfix'
- page.find(".suggest-colors a", match: :first).click
- click_button 'Create'
+ it 'shows error message if label title is taken' do
+ page.within('.block.labels') do
+ fill_in 'new_label_name', with: label.title
+ page.find('.suggest-colors a', match: :first).click
+ click_button 'Create'
- page.within('.dropdown-page-one') do
- expect(page).to have_content 'wontfix'
+ page.within('.dropdown-page-two') do
+ expect(page).to have_content 'Title has already been taken'
+ end
end
end
end
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
new file mode 100644
index 00000000000..4bc9b49f889
--- /dev/null
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+describe 'New issue', feature: true do
+ include StubENV
+
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user)}
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+ current_application_settings.update!(
+ akismet_enabled: true,
+ akismet_api_key: 'testkey',
+ recaptcha_enabled: true,
+ recaptcha_site_key: 'test site key',
+ recaptcha_private_key: 'test private key'
+ )
+
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue after solving reCaptcha' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
+ # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('issue title')
+ expect(page).to have_css('.recaptcha')
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+
+ context 'when not identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 394eb31aff8..1c8267b1593 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Issues', feature: true do
+ include DropzoneHelper
include IssueHelpers
include SortingHelper
include WaitForAjax
@@ -78,8 +79,8 @@ describe 'Issues', feature: true do
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
- page.within '.ui-datepicker' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -110,8 +111,8 @@ describe 'Issues', feature: true do
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
- page.within '.ui-datepicker' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -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,
@@ -382,7 +383,9 @@ describe 'Issues', feature: true do
it 'changes incoming email address token', js: true do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
- find('.incoming-email-token-reset').click
+ find('.incoming-email-token-reset').trigger('click')
+
+ wait_for_ajax
expect(page).to have_no_field('issue_email', with: previous_token)
new_token = project1.new_issue_address(@user.reload)
@@ -568,13 +571,16 @@ describe 'Issues', feature: true do
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
@@ -624,8 +630,8 @@ describe 'Issues', feature: true do
page.within '.due_date' do
click_link 'Edit'
- page.within '.ui-datepicker-calendar' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
wait_for_ajax
@@ -635,11 +641,13 @@ describe 'Issues', feature: true do
end
it 'removes due date from issue' do
+ date = Date.today.at_beginning_of_month + 2.days
+
page.within '.due_date' do
click_link 'Edit'
- page.within '.ui-datepicker-calendar' do
- first('.ui-state-default').click
+ page.within '.pika-single' do
+ click_button date.day
end
wait_for_ajax
@@ -652,25 +660,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 ab7d89306db..f32d1f78b40 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -32,6 +32,34 @@ feature 'Login', feature: true do
end
end
+ describe 'with a blocked account' do
+ it 'prevents the user from logging in' do
+ user = create(:user, :blocked)
+
+ login_with(user)
+
+ expect(page).to have_content('Your account has been blocked.')
+ end
+
+ it 'does not update Devise trackable attributes' do
+ user = create(:user, :blocked)
+
+ expect { login_with(user) }.not_to change { user.reload.sign_in_count }
+ 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/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index c73065cdce1..eafcab6a0d7 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -10,10 +10,12 @@ feature 'Merge Request closing issues message', feature: true do
:merge_request,
:simple,
source_project: project,
- description: merge_request_description
+ description: merge_request_description,
+ title: merge_request_title
)
end
let(:merge_request_description) { 'Merge Request Description' }
+ let(:merge_request_title) { 'Merge Request Title' }
before do
project.team << [user, :master]
@@ -45,8 +47,32 @@ feature 'Merge Request closing issues message', feature: true do
end
end
- context 'closing some issues and mentioning, but not closing, others' do
- let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
+ context 'closing some issues in title and mentioning, but not closing, others' do
+ let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ end
+ end
+
+ context 'closing issues using title but not mentioning any other issue' do
+ let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ end
+ end
+
+ context 'mentioning issues using title but not closing them' do
+ let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
+
+ it 'does not display closing issue message' do
+ expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
+ end
+ end
+
+ context 'closing some issues using title and mentioning, but not closing, others' do
+ let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 5bc4ab2dfe5..18508a44184 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
click_on 'Changes'
wait_for_ajax
- find('.click-to-expand').click
+ click_link 'Expand all'
wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
@@ -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/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index f1b68a39343..0832a3656a8 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Create New Merge Request', feature: true, js: true do
+ include WaitForVueResource
+
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -84,4 +86,25 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
+
+ context 'when a new merge request has a pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, sha: project.commit('fix').id,
+ ref: 'fix',
+ project: project)
+ end
+
+ it 'shows pipelines for a new merge request' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace, project,
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ page.within('.merge-request') do
+ click_link 'Pipelines'
+ wait_for_vue_resource
+
+ expect(page).to have_content "##{pipeline.id}"
+ end
+ end
+ 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_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 4c60329865c..55f3c1863ff 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
include WaitForAjax
let(:project) { create(:project, :public) }
@@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context 'filter by label bug' do
before do
- select_labels('bug')
+ input_filtered_search('label:~bug')
end
it 'apply the filter' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
- expect(find('.filtered-labels')).to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "feature"
- expect(find('.filtered-labels')).not_to have_content "enhancement"
-
- find('.js-label-filter-remove').click
- wait_for_ajax
- expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end
end
context 'filter by label feature' do
before do
- select_labels('feature')
+ input_filtered_search('label:~feature')
end
it 'applies the filter' do
expect(page).to have_content "Feature1"
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).to have_content "feature"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "enhancement"
end
end
context 'filter by label enhancement' do
before do
- select_labels('enhancement')
+ input_filtered_search('label:~enhancement')
end
it 'applies the filter' do
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "feature"
end
end
context 'filter by label enhancement and bug in issues list' do
before do
- select_labels('bug', 'enhancement')
+ input_filtered_search('label:~bug label:~enhancement')
end
it 'applies the filters' do
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
- expect(find('.filtered-labels')).to have_content "bug"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "feature"
-
- find('.js-label-filter-remove', match: :first).click
- wait_for_ajax
-
- expect(page).to have_content "Bugfix2"
- expect(page).not_to have_content "Feature1"
- expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "feature"
end
end
- context 'remove filtered labels' do
+ context 'clear button' do
before do
- page.within '.labels-filter' do
- click_button 'Label'
- wait_for_ajax
- click_link 'bug'
- find('.dropdown-menu-close').click
- end
-
- page.within '.filtered-labels' do
- expect(page).to have_content 'bug'
- end
+ input_filtered_search('label:~bug')
end
it 'allows user to remove filtered labels' do
- first('.js-label-filter-remove').click
- wait_for_ajax
+ first('.clear-search').click
+ filtered_search.send_keys(:enter)
- expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
- expect(find('.labels-filter')).not_to have_content 'bug'
+ expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
+ expect(page).to have_content "Bugfix2"
+ expect(page).to have_content "Feature1"
+ expect(page).to have_content "Bugfix1"
end
end
- context 'dropdown filtering' do
+ context 'filter dropdown' do
it 'filters by label name' do
- page.within '.labels-filter' do
- click_button 'Label'
- wait_for_ajax
- find('.dropdown-input input').set 'bug'
-
- page.within '.dropdown-content' do
- expect(page).not_to have_content 'enhancement'
- expect(page).to have_content 'bug'
- end
- end
- end
- end
+ init_label_search
+ filtered_search.send_keys('~bug')
- def select_labels(*labels)
- page.find('.js-label-select').click
- wait_for_ajax
- labels.each do |label|
- execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
+ page.within '.filter-dropdown' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
end
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
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 f6e9230c8da..265a0cfc198 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -1,10 +1,18 @@
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
+
let(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) }
+ def filter_by_milestone(title)
+ find(".js-milestone-select").click
+ find(".milestone-filter a", text: title).click
+ end
+
before do
project.team << [user, :master]
login_as(user)
@@ -15,42 +23,45 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::None.title)
+ 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
context 'filters by upcoming milestone', js: true do
- it 'does not show issues with no expiry' do
+ it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
- it 'shows issues in future' do
+ it 'shows merge requests in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
- it 'does not show issues in past' do
+ it 'does not show merge requests in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
@@ -61,7 +72,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project)
visit_merge_requests(project)
- filter_by_milestone(milestone.title)
+ input_filtered_search("milestone:%'#{milestone.title}'")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
@@ -77,19 +88,10 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project)
visit_merge_requests(project)
- filter_by_milestone(milestone.title)
+ input_filtered_search("milestone:%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
end
-
- def visit_merge_requests(project)
- visit namespace_project_merge_requests_path(project.namespace, project)
- end
-
- def filter_by_milestone(title)
- find(".js-milestone-select").click
- find(".milestone-filter a", text: title).click
- end
end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 4642b5a530d..70e3997e716 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -1,11 +1,13 @@
require 'rails_helper'
describe 'Filter merge requests', feature: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) }
- let!(:user) { create(:user)}
+ let!(:user) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
@@ -15,183 +17,162 @@ describe 'Filter merge requests', feature: true do
group.add_developer(user)
login_as(user)
create(:merge_request, source_project: project, target_project: project)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
end
describe 'for assignee from mr#index' do
- before do
- visit namespace_project_merge_requests_path(project.namespace, project)
+ let(:search_query) { "assignee:@#{user.username}" }
- find('.js-assignee-search').click
+ def expect_assignee_visual_tokens
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
+ end
- find('.dropdown-menu-user-link', text: user.username).click
+ before do
+ input_filtered_search(search_query)
- wait_for_ajax
+ expect_mr_list_count(0)
end
context 'assignee', js: true do
it 'updates to current user' do
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect_assignee_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect_assignee_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect_assignee_visual_tokens()
end
end
end
describe 'for milestone from mr#index' do
- before do
- visit namespace_project_merge_requests_path(project.namespace, project)
+ let(:search_query) { "milestone:%\"#{milestone.title}\"" }
- find('.js-milestone-select').click
+ def expect_milestone_visual_tokens
+ expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
+ expect_filtered_search_input_empty
+ end
- find('.milestone-filter .dropdown-content a', text: milestone.title).click
+ before do
+ input_filtered_search(search_query)
- wait_for_ajax
+ expect_mr_list_count(0)
end
context 'milestone', js: true do
it 'updates to current milestone' do
- expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ expect_milestone_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ expect_milestone_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ expect_milestone_visual_tokens()
end
end
end
describe 'for label from mr#index', js: true do
- before do
- visit namespace_project_merge_requests_path(project.namespace, project)
- find('.js-label-select').click
- wait_for_ajax
- end
-
- it 'filters by any label' do
- find('.dropdown-menu-labels a', text: 'Any Label').click
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
-
- expect(find('.labels-filter')).to have_content 'Label'
- end
-
it 'filters by no label' do
- find('.dropdown-menu-labels a', text: 'No Label').click
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ input_filtered_search('label:none')
- page.within '.labels-filter' do
- expect(page).to have_content 'Labels'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
+ expect_mr_list_count(1)
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
it 'filters by a label' do
- find('.dropdown-menu-labels a', text: label.title).click
- page.within '.labels-filter' do
- expect(page).to have_content label.title
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ input_filtered_search("label:~#{label.title}")
+
+ expect_mr_list_count(0)
+ expect_tokens([{ name: 'label', value: "~#{label.title}" }])
+ expect_filtered_search_input_empty
end
it "filters by `won't fix` and another label" do
- page.within '.labels-filter' do
- click_link wontfix.title
- expect(page).to have_content wontfix.title
- click_link label.title
- end
-
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
+ input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
+
+ expect_mr_list_count(0)
+ 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
- page.within '.labels-filter' do
- click_link wontfix.title
- expect(page).to have_content wontfix.title
- end
-
- find('body').click
+ input_filtered_search("label:~\"#{wontfix.title}\"")
- expect(find('.filtered-labels')).to have_content(wontfix.title)
+ expect_mr_list_count(0)
+ expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
+ expect_filtered_search_input_empty
- find('.js-label-select').click
- wait_for_ajax
- find('.dropdown-menu-labels a', text: label.title).click
-
- find('body').click
-
- expect(find('.filtered-labels')).to have_content(wontfix.title)
- expect(find('.filtered-labels')).to have_content(label.title)
-
- find('.js-label-select').click
- wait_for_ajax
+ input_filtered_search_keys("label:~#{label.title}")
- expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
- expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
- end
-
- it "selects and unselects `won't fix`" do
- find('.dropdown-menu-labels a', text: wontfix.title).click
- find('.dropdown-menu-labels a', text: wontfix.title).click
- # Close label dropdown to load
- find('body').click
- expect(page).not_to have_css('.filtered-labels')
+ expect_mr_list_count(0)
+ expect_tokens([
+ { name: 'label', value: "~\"#{wontfix.title}\"" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
end
end
describe 'for assignee and label from issues#index' do
- before do
- visit namespace_project_merge_requests_path(project.namespace, project)
-
- find('.js-assignee-search').click
+ let(:search_query) { "assignee:@#{user.username} label:~#{label.title}" }
- find('.dropdown-menu-user-link', text: user.username).click
+ before do
+ input_filtered_search("assignee:@#{user.username}")
- expect(page).not_to have_selector('.mr-list .merge-request')
+ expect_mr_list_count(1)
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
- find('.js-label-select').click
+ input_filtered_search_keys("label:~#{label.title} ")
- find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ expect_mr_list_count(1)
- wait_for_ajax
+ find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]")
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(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ 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(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ 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(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ expect_assignee_label_visual_tokens()
end
end
end
@@ -203,11 +184,11 @@ describe 'Filter merge requests', feature: true do
bug_label = create(:label, project: project, title: 'bug')
milestone = create(:milestone, title: "8", project: project)
- mr = create(:merge_request,
- title: "Bug 2",
- source_project: project,
- target_project: project,
- source_branch: "bug2",
+ mr = create(:merge_request,
+ title: "Bug 2",
+ source_project: project,
+ target_project: project,
+ source_branch: "bug2",
milestone: milestone,
author: user,
assignee: user)
@@ -218,15 +199,13 @@ describe 'Filter merge requests', feature: true do
context 'only text', js: true do
it 'filters merge requests by searched text' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('bug')
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
end
it 'does not show any merge requests' do
- fill_in 'issuable_search', with: 'testing'
+ input_filtered_search('testing')
page.within '.mr-list' do
expect(page).not_to have_selector('.merge-request')
@@ -234,82 +213,57 @@ describe 'Filter merge requests', feature: true do
end
end
- context 'text and dropdown options', js: true do
+ context 'filters and searches', js: true do
it 'filters by text and label' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('Bug')
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Label'
- page.within '.labels-filter' do
- click_link 'bug'
- end
- find('.dropdown-menu-close-icon').click
+ input_filtered_search_keys(' label:~bug')
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 1)
- end
+ expect_mr_list_count(1)
+ expect_tokens([{ name: 'label', value: '~bug' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and milestone' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('Bug')
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Milestone'
- page.within '.milestone-filter' do
- click_link '8'
- end
+ input_filtered_search_keys(' milestone:%8')
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 1)
- end
+ expect_mr_list_count(1)
+ expect_tokens([{ name: 'milestone', value: '%8' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and assignee' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('Bug')
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Assignee'
- page.within '.dropdown-menu-assignee' do
- click_link user.name
- end
+ input_filtered_search_keys(" assignee:@#{user.username}")
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 1)
- end
+ 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
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('Bug')
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Author'
- page.within '.dropdown-menu-author' do
- click_link user.name
- end
+ input_filtered_search_keys(" author:@#{user.username}")
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 1)
- end
+ expect_mr_list_count(1)
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input('Bug')
end
end
end
@@ -328,18 +282,9 @@ describe 'Filter merge requests', feature: true do
end
it 'is able to filter and sort merge requests' do
- click_button 'Label'
- wait_for_ajax
- page.within '.labels-filter' do
- click_link 'bug'
- end
- find('.dropdown-menu-close-icon').click
- wait_for_ajax
+ input_filtered_search('label:~bug')
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
click_button 'Last created'
page.within '.dropdown-menu-sort' do
@@ -352,4 +297,42 @@ describe 'Filter merge requests', feature: true do
end
end
end
+
+ describe 'filter by assignee id', js: true do
+ it 'filter by current user' do
+ visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
+
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'filter by new user' do
+ new_user = create(:user)
+ project.add_developer(new_user)
+
+ visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
+
+ expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ describe 'filter by author id', js: true do
+ it 'filter by current user' do
+ visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
+
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'filter by new user' do
+ new_user = create(:user)
+ project.add_developer(new_user)
+
+ visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
+
+ 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/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..84ad8765d8f
--- /dev/null
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -0,0 +1,100 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.run
+
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ describe 'build list toggle' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ it 'should expand when hovered' do
+ before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ toggle.hover
+
+ after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ expect(before_width).to be < after_width
+ end
+
+ it 'should show dropdown caret when hovered' do
+ toggle.hover
+
+ expect(toggle).to have_selector('.fa-caret-down')
+ end
+
+ it 'should show tooltip when hovered' do
+ toggle.hover
+
+ expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+
+ describe 'builds list menu' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ before do
+ toggle.click
+ wait_for_ajax
+ end
+
+ it 'should open when toggle is clicked' do
+ expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when toggle is clicked again' do
+ toggle.trigger('click')
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when clicking somewhere else' do
+ find('body').click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ describe 'build list build item' do
+ let(:build_item) do
+ find('.mini-pipeline-graph-dropdown-item')
+ first('.mini-pipeline-graph-dropdown-item')
+ end
+
+ it 'should visit the build page when clicked' do
+ build_item.click
+ find('.build-page')
+
+ expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
+ end
+
+ it 'should show tooltip when hovered' do
+ build_item.hover
+
+ expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+ end
+end
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 7e2907cd26f..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
@@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -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 3a7ece7e1d6..6fed1568fcf 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,17 +1,20 @@
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
include IssueHelpers
- let!(:project) { create(:project, :public) }
- let!(:user) { create(:user)}
- let!(:milestone) { create(:milestone, project: project) }
- let!(:bug) { create(:label, project: project, name: 'bug')}
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:bug) { create(:label, project: project, name: 'bug')}
let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
- let(:merge_request_css) { '.merge-request' }
+ let(:merge_request_css) { '.merge-request' }
+ let(:clear_search_css) { '.filtered-search-input-container .clear-search' }
before do
mr2.labels << bug
@@ -21,76 +24,94 @@ 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 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_id: user.id)
+ 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_id: user.id)
+ 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
- visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+ 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(page).not_to have_css '.reset_filters'
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
+ expect(page).not_to have_css(clear_search_css)
end
end
-
- def visit_merge_requests(project, opts = {})
- visit namespace_project_merge_requests_path project.namespace, project, opts
- end
-
- def reset_filters
- find('.reset-filters').click
- end
end
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index 44a9b545ff8..a2cf9b18bf2 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -10,8 +10,8 @@ feature 'toggler_behavior', js: true, feature: true do
before do
login_as :admin
project = merge_request.source_project
- visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"
page.current_window.resize_to(1000, 300)
+ visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"
end
describe 'scroll position' do
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 2582a540240..2f3c3e45ae6 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -120,5 +120,81 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).not_to have_content '/due 2016-08-28'
end
end
+
+ describe '/target_branch command in merge request' do
+ let(:another_project) { create(:project, :public) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+
+ before do
+ logout
+ another_project.team << [user, :master]
+ login_with(user)
+ end
+
+ 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:"
+ click_button "Submit merge request"
+
+ merge_request = another_project.merge_requests.first
+ expect(merge_request.description).to eq "le feature \nFeature description:"
+ expect(merge_request.target_branch).to eq 'fix'
+ end
+
+ it 'does not change target branch when merge request is edited' do
+ new_merge_request = create(:merge_request, source_project: another_project)
+
+ visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request)
+ fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
+ click_button "Save changes"
+
+ new_merge_request = another_project.merge_requests.first
+ expect(new_merge_request.description).to include('/target_branch')
+ expect(new_merge_request.target_branch).not_to eq('fix')
+ end
+ end
+
+ describe '/target_branch command from note' do
+ context 'when the current user can change target branch' do
+ it 'changes target branch from a note' do
+ write_note("message start \n/target_branch merge-test\n message end.")
+
+ expect(page).not_to have_content('/target_branch')
+ expect(page).to have_content('message start')
+ expect(page).to have_content('message end.')
+
+ expect(merge_request.reload.target_branch).to eq 'merge-test'
+ end
+
+ it 'does not fail when target branch does not exists' do
+ write_note('/target_branch totally_not_existing_branch')
+
+ expect(page).not_to have_content('/target_branch')
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+
+ context 'when current user can not change target branch' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not change target branch' do
+ write_note('/target_branch merge-test')
+
+ expect(page).not_to have_content '/target_branch merge-test'
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+ end
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 7d1805f5001..c2db7d8da3c 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -3,32 +3,156 @@ 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
project.team << [user, :master]
login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ )
+ end
+
+ it 'shows widget status after creating new merge request' do
+ click_button 'Submit merge request'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.accept-merge-request')
+ end
+ end
+
+ context 'view merge request' do
+ let!(:environment) { create(:environment, project: project) }
+
+ 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)
+ end
- visit new_namespace_project_merge_request_path(
- project.namespace,
- project,
- merge_request: {
- source_project_id: project.id,
- target_project_id: project.id,
- source_branch: 'feature',
- target_branch: 'master'
- }
- )
+ it 'shows environments link' do
+ wait_for_ajax
+
+ page.within('.mr-widget-heading') do
+ expect(page).to have_content("Deployed to #{environment.name}")
+ 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
- it 'shows widget status after creating new merge request' do
- click_button 'Submit merge request'
+ 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
- expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically')
+ 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
- wait_for_ajax
+ context 'merge error' do
+ before do
+ allow_any_instance_of(Repository).to receive(:merge).and_return(false)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ click_button 'Accept Merge Request'
+ wait_for_ajax
+ end
- expect(page).to have_selector('.accept_merge_request')
+ it 'updates the MR widget' do
+ page.within('.mr-widget-body') do
+ expect(page).to have_content('Conflicts detected during merge')
+ end
+ end
end
end
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/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index aadd72a9f8e..8de9942c54e 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
include WaitForAjax
+ include DragTo
let(:milestone) { create(:milestone, project: project, title: 8.14) }
let(:project) { create(:empty_project, :public) }
@@ -75,7 +76,7 @@ describe 'Milestone draggable', feature: true, js: true do
create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
visit namespace_project_milestone_path(project.namespace, project, milestone)
- issue.drag_to(issue_target)
+ drag_to(selector: '.issues-sortable-list', list_to_index: 1)
wait_for_ajax
end
@@ -85,7 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
- merge_request.drag_to(merge_request_target)
+ drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
wait_for_ajax
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 55a01057c83..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
@@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
# Set date to 1st of next month
find_field("Expires at").trigger('focus')
- find("a[title='Next']").click
+ find(".pika-next").click
click_on "1"
# Scopes
@@ -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/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index a6b841c0210..15c8677fcd3 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do
visit profile_preferences_path
end
- describe 'User changes their application theme', js: true do
- let(:default) { Gitlab::Themes.default }
- let(:theme) { Gitlab::Themes.by_id(5) }
-
- it 'creates a flash message' do
- choose "user_theme_id_#{theme.id}"
-
- expect_preferences_saved_message
- end
-
- it 'updates their preference' do
- choose "user_theme_id_#{theme.id}"
-
- allowing_for_delay do
- visit page.current_path
- expect(page).to have_checked_field("user_theme_id_#{theme.id}")
- end
- end
-
- it 'reflects the changes immediately' do
- expect(page).to have_selector("body.#{default.css_class}")
-
- choose "user_theme_id_#{theme.id}"
-
- expect(page).not_to have_selector("body.#{default.css_class}")
- expect(page).to have_selector("body.#{theme.css_class}")
- end
- end
-
describe 'User changes their syntax highlighting theme', js: true do
it 'creates a flash message' do
choose 'user_color_scheme_id_5'
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
new file mode 100644
index 00000000000..e05fbb3715c
--- /dev/null
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -0,0 +1,32 @@
+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/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 67a4a5d1ab1..ae9db0c0d6e 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -14,7 +14,8 @@ feature 'list of badges' do
expect(page).to have_content 'build status'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_content 'AsciiDoc'
+ expect(page).to have_css('.highlight', count: 3)
expect(page).to have_xpath("//img[@alt='build status']")
page.within('.highlight', match: :first) do
@@ -28,7 +29,8 @@ feature 'list of badges' do
expect(page).to have_content 'coverage report'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_content 'AsciiDoc'
+ expect(page).to have_css('.highlight', count: 3)
expect(page).to have_xpath("//img[@alt='coverage report']")
page.within('.highlight', match: :first) do
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/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
new file mode 100644
index 00000000000..30e2d587267
--- /dev/null
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Blob shortcuts', feature: true do
+ include TreeHelper
+ let(:project) { create(:project, :public, :repository) }
+ let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
+ let(:sha) { project.repository.commit.sha }
+
+ describe 'On a file(blob)', js: true 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 'pressing "y"' do
+ it 'redirects to permalink with commit sha' do
+ visit_blob
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true)
+ end
+
+ it 'maintains fragment hash when redirecting' do
+ fragment = "L1"
+ visit_blob(fragment)
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true)
+ 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/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 11d27feab0b..2116721b224 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -27,7 +27,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :pending)
end
- it "shows Pending tab builds" do
+ it "shows Pending tab jobs" do
expect(page).to have_link 'Cancel running'
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content build.short_sha
@@ -42,7 +42,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :running)
end
- it "shows Running tab builds" do
+ it "shows Running tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_link 'Cancel running'
expect(page).to have_content build.short_sha
@@ -57,20 +57,20 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :finished)
end
- it "shows Finished tab builds" do
+ it "shows Finished tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel running'
end
end
- context "All builds" do
+ context "All jobs" do
before do
project.builds.running_or_pending.each(&:success)
visit namespace_project_builds_path(project.namespace, project)
end
- it "shows All tab builds" do
+ it "shows All tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content build.short_sha
expect(page).to have_content build.ref
@@ -98,7 +98,7 @@ feature 'Builds', :feature do
end
describe "GET /:project/builds/:id" do
- context "Build from project" do
+ context "Job from project" do
before do
visit namespace_project_build_path(project.namespace, project, build)
end
@@ -109,9 +109,13 @@ feature 'Builds', :feature do
expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.git_author_name
end
+
+ it 'shows active build' do
+ expect(page).to have_selector('.build-job.active')
+ end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
visit namespace_project_build_path(project.namespace, project, build2)
end
@@ -149,7 +153,7 @@ feature 'Builds', :feature do
context 'when expire date is defined' do
let(:expire_at) { Time.now + 7.days }
- context 'when user has ability to update build' do
+ context 'when user has ability to update job' do
it 'keeps artifacts when keep button is clicked' do
expect(page).to have_content 'The artifacts will be removed'
@@ -160,7 +164,7 @@ feature 'Builds', :feature do
end
end
- context 'when user does not have ability to update build' do
+ context 'when user does not have ability to update job' do
let(:user_access_level) { :guest }
it 'does not have keep button' do
@@ -197,8 +201,8 @@ feature 'Builds', :feature do
visit namespace_project_build_path(project.namespace, project, build)
end
- context 'when build has an initial trace' do
- it 'loads build trace' do
+ context 'when job has an initial trace' do
+ it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
build.append_trace(' and more trace', 11)
@@ -242,36 +246,36 @@ feature 'Builds', :feature do
end
end
- context 'when build starts environment' do
+ context 'when job starts environment' do
let(:environment) { create(:environment, project: project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
- context 'build is successfull and has deployment' do
+ context 'job is successfull and has deployment' do
let(:deployment) { create(:deployment) }
let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build is complete and not successfull' do
+ context 'job is complete and not successfull' do
let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build creates a new deployment' do
+ context 'job creates a new deployment' do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
- it 'shows a link to lastest deployment' do
+ it 'shows a link to latest deployment' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link('latest deployment')
@@ -281,7 +285,7 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/cancel" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -295,7 +299,7 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -307,13 +311,13 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/retry" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
click_link 'Cancel'
page.within('.build-header') do
- click_link 'Retry build'
+ click_link 'Retry job'
end
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 33f1c323af1..268d420c594 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit pipelines' do
+feature 'project commit pipelines', js: true do
given(:project) { create(:project) }
background do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
new file mode 100644
index 00000000000..0b972d2a439
--- /dev/null
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+include WaitForAjax
+
+describe 'Cherry-pick Commits' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ end
+
+ context "I cherry-pick a commit" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ expect(page).not_to have_content('v1.0.0') # Only branches, not tags
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context "I cherry-pick a merge commit" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context "I cherry-pick a commit that was previously cherry-picked" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+
+ context "I cherry-pick a commit in a new merge request" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') 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
+
+ context "I cherry-pick a commit from a different branch", js: true do
+ it do
+ find('.header-action-buttons a.dropdown-toggle').click
+ find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'master'
+ end
+
+ wait_for_ajax
+
+ page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+ click_link 'feature'
+ end
+
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ 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/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
deleted file mode 100644
index 7baf7913424..00000000000
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-include WaitForAjax
-
-describe 'Cherry-pick Commits' do
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
- let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
- let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
-
- before do
- login_as :user
- project.team << [@user, :master]
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
- end
-
- context "I cherry-pick a commit" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- expect(page).not_to have_content('v1.0.0') # Only branches, not tags
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('The commit has been successfully cherry-picked.')
- end
- end
-
- context "I cherry-pick a merge commit" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('The commit has been successfully cherry-picked.')
- end
- end
-
- context "I cherry-pick a commit that was previously cherry-picked" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
- end
- end
-
- context "I cherry-pick a commit in a new merge request" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') 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.')
- end
- end
-
- context "I cherry-pick a commit from a different branch", js: true do
- it do
- find('.header-action-buttons a.dropdown-toggle').click
- find(:css, "a[href='#modal-cherry-pick-commit']").click
-
- page.within('#modal-cherry-pick-commit') do
- click_button 'master'
- end
-
- wait_for_ajax
-
- page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
- click_link 'feature'
- end
-
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
-
- expect(page).to have_content('The commit has been successfully cherry-picked.')
- end
- end
-end
diff --git a/spec/features/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..43eb4000e58 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
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/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index fe047e00409..36a80d7575d 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do
let(:user) { create(:user) }
let(:commit_params) do
{
- source_branch: project.default_branch,
+ start_branch: project.default_branch,
target_branch: project.default_branch,
commit_message: "Committing First Update",
file_path: ".gitignore",
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index fc88fd74af8..582349d8d5b 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -22,7 +22,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do
expect(page).to have_selector('.blob-content-holder')
- page.within('.file-title') do
+ page.within('.js-file-title') do
expect(page).to have_content('CHANGELOG')
end
end
@@ -35,7 +35,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do
expect(page).to have_selector('.blob-content-holder')
- page.within('.file-title') do
+ page.within('.js-file-title') do
expect(page).to have_content('application.js')
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 a521ce50f35..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,8 @@ 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', 'Remove LICENSE', 'master')
+ project.repository.delete_file(project_master, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
visit namespace_project_path(project.namespace, project)
@@ -24,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
@@ -32,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
@@ -48,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
@@ -56,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/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 6dae5c64b30..62d0aedda48 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,8 +18,18 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
- project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
- project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master')
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/test.md',
+ longtemplate_content,
+ message: 'added issue template',
+ branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -67,7 +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(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.create_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ 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
@@ -86,7 +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(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.create_file(
+ user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ 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
@@ -111,7 +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(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.create_file(
+ fork_user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ 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..de3c6eceb82 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -32,12 +32,12 @@ feature 'Issue prioritization', feature: true do
visit namespace_project_issues_path(project.namespace, project, sort: '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
@@ -70,14 +70,14 @@ feature 'Issue prioritization', feature: true do
login_as user
visit namespace_project_issues_path(project.namespace, project, sort: '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/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 97ce9cdfd87..1e900d7e660 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do
include WaitForAjax
+ include DragTo
let(:user) { create(:user) }
let(:group) { create(:group) }
@@ -99,7 +100,7 @@ feature 'Prioritize labels', feature: true do
expect(page).to have_content 'wontfix'
# Sort labels
- find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
+ drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
expect(first('li')).to have_content('feature')
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
index 227ccf9459c..02198ff3e41 100644
--- a/spec/features/projects/main/download_buttons_spec.rb
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do
expect(page).to have_link "Download '#{build.name}'", href: href
end
+
+ scenario 'download links have download attribute' do
+ expect(page).to have_selector('a', text: 'Download')
+ page.all('a', text: 'Download').each do |link|
+ expect(link[:download]).to eq ''
+ end
+ end
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/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index f136d9ce0fa..c3f45be6e4b 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -14,15 +14,16 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
- scenario 'expiration date is displayed in the members list', js: true do
+ scenario 'expiration date is displayed in the members list' do
travel_to Time.zone.parse('2016-08-06 08:00') do
- visit namespace_project_settings_members_path(project.namespace, project)
+ date = 4.days.from_now
+ visit namespace_project_project_members_path(project.namespace, project)
+
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
- fill_in 'expires_at', with: '2016-08-10'
+ fill_in 'expires_at', with: date.to_s(:medium)
+ click_on 'Add to project'
end
- find('.users-project-form').click
- click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
@@ -32,11 +33,12 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
scenario 'change expiration date' do
travel_to Time.zone.parse('2016-08-06 08:00') do
- project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ date = 3.days.from_now
+ project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium))
visit namespace_project_project_members_path(project.namespace, project)
page.within "#project_member_#{new_member.project_members.first.id}" do
- find('.js-access-expiration-date').set '2016-08-09'
+ find('.js-access-expiration-date').set date.to_s(:medium)
wait_for_ajax
expect(page).to have_content('Expires in 3 days')
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 b56e562b2b6..45185f2dd1f 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -19,6 +19,51 @@ feature "New project", feature: true do
end
end
+ context "Namespace selector" do
+ context "with user namespace" do
+ before do
+ visit new_project_path
+ end
+
+ it "selects the user namespace" do
+ namespace = find("#project_namespace_id")
+
+ expect(namespace.text).to eq user.username
+ end
+ end
+
+ context "with group namespace" do
+ let(:group) { create(:group, :private, owner: user) }
+
+ before do
+ group.add_owner(user)
+ visit new_project_path(namespace_id: group.id)
+ end
+
+ it "selects the group namespace" do
+ namespace = find("#project_namespace_id option[selected]")
+
+ expect(namespace.text).to eq group.name
+ end
+
+ context "on validation error" do
+ before do
+ fill_in('project_path', with: 'private-group-project')
+ choose('Internal')
+ click_button('Create project')
+
+ expect(page).to have_css '.project-edit-errors .alert.alert-danger'
+ end
+
+ it "selects the group namespace" do
+ namespace = find("#project_namespace_id option[selected]")
+
+ expect(namespace.text).to eq group.name
+ end
+ end
+ end
+ end
+
context 'Import project options' do
before do
visit new_project_path
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
new file mode 100644
index 00000000000..11793c0f303
--- /dev/null
+++ b/spec/features/projects/pages_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+feature 'Pages', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :master }
+
+ background do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.team << [user, role]
+
+ login_as(user)
+ end
+
+ shared_examples 'no pages deployed' do
+ scenario 'does not see anything to destroy' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).not_to have_link('Remove pages')
+ expect(page).not_to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ context 'when user is the owner' do
+ background do
+ project.namespace.update(owner: user)
+ end
+
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Remove pages" link' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_link('Remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+
+ context 'when the user is not the owner' do
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Only the project owner can remove pages" text' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 917b545e98b..9f06e52ab55 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -54,7 +54,7 @@ describe 'Pipeline', :feature, :js do
expect(page).to have_content('Build')
expect(page).to have_content('Test')
expect(page).to have_content('Deploy')
- expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -91,10 +91,10 @@ describe 'Pipeline', :feature, :js do
end
end
- it 'should be possible to retry the success build' do
+ it 'should be possible to retry the success job' do
find('#ci-badge-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
@@ -113,11 +113,11 @@ describe 'Pipeline', :feature, :js do
it 'should be possible to retry the failed build' do
find('#ci-badge-test .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
- context 'when pipeline has manual builds' do
+ context 'when pipeline has manual jobs' do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
expect(page).to have_selector('.js-ci-status-icon-manual')
@@ -129,14 +129,14 @@ describe 'Pipeline', :feature, :js do
end
end
- it 'should be possible to play the manual build' do
+ it 'should be possible to play the manual job' do
find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Play build')
+ expect(page).not_to have_content('Play job')
end
end
- context 'when pipeline has external build' do
+ context 'when pipeline has external job' do
it 'shows the success icon and the generic comit status build' do
expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('jenkins')
@@ -146,12 +146,12 @@ describe 'Pipeline', :feature, :js do
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
@@ -160,17 +160,17 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
- before { click_on 'Retry failed' }
+ before { find('.js-retry-button').trigger('click') }
- it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).not_to have_content('Retry') }
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -191,49 +191,49 @@ describe 'Pipeline', :feature, :js do
visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- it 'shows a list of builds' do
+ it 'shows a list of jobs' do
expect(page).to have_content('Test')
expect(page).to have_content(build_passed.id)
expect(page).to have_content('Deploy')
expect(page).to have_content(build_failed.id)
expect(page).to have_content(build_running.id)
expect(page).to have_content(build_external.id)
- expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
end
- it 'shows Builds tab pane as active' do
+ it 'shows jobs tab pane as active' do
expect(page).to have_css('#js-tab-builds.active')
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
- it 'shows Builds tab as active' do
+ it 'shows Jobs tab as active' do
expect(page).to have_css('li.js-builds-tab-link.active')
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
- before { click_on 'Retry failed' }
+ before { find('.js-retry-button').trigger('click') }
- it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).not_to have_content('Retry') }
it { expect(page).to have_selector('.retried') }
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -244,7 +244,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'playing manual build' do
+ context 'playing manual job' do
before do
within '.pipeline-holder' do
click_link('Play')
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ca18ac073d8..22bf1bfbdf0 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -26,18 +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
+
+ [:all, :running, :pending, :finished, :branches].each do |scope|
+ context "when displaying #{scope}" do
+ before do
+ visit_project_pipelines(scope: scope)
+ end
+
+ it 'contains pipeline commit short SHA' do
+ expect(page).to have_content(pipeline.short_sha)
+ end
- it 'contains pipeline commit short SHA' do
- expect(page).to have_content(pipeline.short_sha)
+ 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,
@@ -214,6 +262,14 @@ describe 'Pipelines', :feature, :js do
expect(page).to have_link(with_artifacts.name)
end
+
+ it 'has download attribute on download links' do
+ find('.js-pipeline-dropdown-download').click
+ expect(page).to have_selector('a', text: 'Download')
+ page.all('.build-artifacts a', text: 'Download').each do |link|
+ expect(link[:download]).to eq ''
+ end
+ end
end
context 'with artifacts expired' do
@@ -272,6 +328,39 @@ 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
+ before do
+ allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
+ create(:ci_empty_pipeline, project: project)
+ end
+
+ it 'should render pagination' do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.gl-pagination')
+ end
+
+ it 'should render second page of pipelines' do
+ visit namespace_project_pipelines_path(project.namespace, project, page: '2')
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
end
end
@@ -282,8 +371,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 }
@@ -300,15 +395,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
@@ -320,18 +406,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/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 472491188c9..3b8f0b2d3f8 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -17,14 +17,17 @@ feature 'Ref switcher', feature: true, js: true do
page.within '.project-refs-form' do
input = find('input[type="search"]')
- input.set 'expand'
+ input.set 'binary'
+ wait_for_ajax
- input.native.send_keys :down
- input.native.send_keys :down
- input.native.send_keys :enter
+ expect(find('.dropdown-content ul')).to have_selector('li', count: 6)
+
+ page.within '.dropdown-content ul' do
+ input.native.send_keys :enter
+ end
end
- expect(page).to have_title 'expand-collapse-files'
+ expect(page).to have_title 'binary-encoding'
end
it "user selects ref with special characters" do
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 042a1ccab51..24d22a092d4 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
-feature 'Setup Mattermost slash commands', feature: true do
- include WaitForAjax
-
+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 }
@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visits the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do
- wait_for_ajax
+ expect(page).to have_content("This service allows users to perform common")
+ end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content("This service allows GitLab users to perform common")
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
it 'shows the token after saving' do
@@ -60,8 +62,8 @@ 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 be(true)
@@ -73,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
@@ -90,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(false)
- expect(selected_option).to have_content('Select team...')
- # The 'Select team...' placeholder is item `0`.
expect(select_element.all('option').count).to eq(3)
end
@@ -108,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
@@ -135,6 +151,12 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
+
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
end
end
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index 32b32f7ae8e..db903a0c8f0 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Slack slash commands', feature: true do
- include WaitForAjax
-
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do
project.team << [user, :master]
login_as(user)
- end
-
- scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
+ end
- wait_for_ajax
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content('This service allows GitLab users to perform common')
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- scenario 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ it 'shows a help message' do
+ expect(page).to have_content('This service allows users to perform common')
+ end
+ it 'shows the token after saving' do
fill_in 'service_token', with: 'token'
click_on 'Save'
@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token')
end
- scenario 'shows the correct trigger url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
+ it 'shows the correct trigger url' do
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 4bfaa499272..6815039d5ed 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -11,41 +11,36 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
login_as(user)
end
- context 'when Merge Request and Builds are initially enabled' do
- before do
- project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED)
- end
-
- context 'when Builds are initially enabled' do
+ context 'when Merge Request and Pipelines are initially enabled' do
+ context 'when Pipelines are initially enabled' do
before do
- project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings' do
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
- context 'when Builds are initially disabled' do
+ context 'when Pipelines are initially disabled' do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
@@ -58,12 +53,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
end
scenario 'does not show the Merge Requests settings' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
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/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
new file mode 100644
index 00000000000..ce5c5f21167
--- /dev/null
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'View on environment', js: true do
+ include WaitForAjax
+
+ let(:branch_name) { 'feature' }
+ let(:file_path) { 'files/ruby/feature.rb' }
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'when the branch has a route map' do
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /files/(.*)\\..*/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: route_map
+ ).execute
+
+ # Update the file so that we still have a commit that will have a file on the environment
+ Files::UpdateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Update feature",
+ file_path: file_path,
+ file_content: "# Noop"
+ ).execute
+ end
+
+ context 'and an active deployment' do
+ let(:sha) { project.commit(branch_name).sha }
+ let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
+ let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) }
+
+ context 'when visiting the diff of a merge request for the branch' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
+
+ before do
+ login_as(user)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ within '.diffs' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+
+ context 'when visiting a comparison for the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a comparison for the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index b4f5f6b3fc5..20219f3cc9a 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe 'Projects > Wiki > User views wiki in project page', feature: true do
let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
before do
project.team << [user, :master]
@@ -10,12 +9,11 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do
end
context 'when repository is disabled for project' do
- before do
- project.project_feature.update!(
- repository_access_level: ProjectFeature::DISABLED,
- merge_requests_access_level: ProjectFeature::DISABLED,
- builds_access_level: ProjectFeature::DISABLED
- )
+ let(:project) do
+ create(:empty_project,
+ :repository_disabled,
+ :merge_requests_disabled,
+ :builds_disabled)
end
context 'when wiki homepage contains a link' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c30d38b6508..3a1240f95b5 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
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
index 395c61a4743..e4aca25a339 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -26,7 +26,11 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-push").click
- within('.js-allowed-to-push-container') { click_on access_type_name }
+
+ within('.js-allowed-to-push-container') do
+ expect(first("li")).to have_content("Roles")
+ click_on access_type_name
+ end
end
wait_for_ajax
@@ -61,7 +65,11 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-merge").click
- within('.js-allowed-to-merge-container') { click_on access_type_name }
+
+ within('.js-allowed-to-merge-container') do
+ expect(first("li")).to have_content("Roles")
+ click_on access_type_name
+ end
end
wait_for_ajax
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 0fe5a897565..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('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ 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('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ 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 92d5a2fbc48..1a66d1a6a1e 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -96,6 +96,34 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_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/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 b616e488487..ad3bd60a313 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -92,6 +92,34 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_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/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_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/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
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index ded85e837f4..e06aab4e0b2 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -96,6 +96,34 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_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/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/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb
new file mode 100644
index 00000000000..191c2fb9a22
--- /dev/null
+++ b/spec/features/snippets/user_snippets_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'User Snippets', feature: true do
+ let(:author) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: author, title: "This is a public snippet") }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: author, title: "This is an internal snippet") }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: author, title: "This is a private snippet") }
+
+ background do
+ login_as author
+ visit dashboard_snippets_path
+ end
+
+ scenario 'View all of my snippets' do
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).to have_content(private_snippet.title)
+ end
+
+ scenario 'View my public snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Public"
+ end
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'View my internal snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Internal"
+ end
+
+ expect(page).not_to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'View my private snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Private"
+ end
+
+ expect(page).not_to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).to have_content(private_snippet.title)
+ end
+end
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/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index d1f2bc78884..e8f06916d53 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -98,15 +98,58 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
expect(find('.todos-list')).not_to have_content merge_request.to_reference
end
- it 'filters by action' do
- click_button 'Action'
- within '.dropdown-menu-action' do
- click_link 'Assigned'
+ describe 'filter by action' do
+ before do
+ create(:todo, :build_failed, user: user_1, author: user_2, project: project_1)
+ create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue)
end
- wait_for_ajax
+ it 'filters by Assigned' do
+ filter_action('Assigned')
+
+ expect_to_see_action(:assigned)
+ end
+
+ it 'filters by Mentioned' do
+ filter_action('Mentioned')
+
+ expect_to_see_action(:mentioned)
+ end
+
+ it 'filters by Added' do
+ filter_action('Added')
+
+ expect_to_see_action(:marked)
+ end
+
+ it 'filters by Pipelines' do
+ filter_action('Pipelines')
- expect(find('.todos-list')).to have_content ' assigned you '
- expect(find('.todos-list')).not_to have_content ' mentioned '
+ expect_to_see_action(:build_failed)
+ end
+
+ def filter_action(name)
+ click_button 'Action'
+ within '.dropdown-menu-action' do
+ click_link name
+ end
+
+ wait_for_ajax
+ end
+
+ def expect_to_see_action(action_name)
+ action_names = {
+ assigned: ' assigned you ',
+ mentioned: ' mentioned ',
+ marked: ' added a todo for ',
+ build_failed: ' build failed for '
+ }
+
+ action_name_text = action_names.delete(action_name)
+ expect(find('.todos-list')).to have_content action_name_text
+ action_names.each_value do |other_action_text|
+ expect(find('.todos-list')).not_to have_content other_action_text
+ end
+ end
end
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 1b352be9331..5c2df949ac5 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Dashboard Todos', feature: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
@@ -34,39 +36,97 @@ describe 'Dashboard Todos', feature: true do
end
end
- describe 'deleting the todo' do
+ shared_examples 'deleting the todo' do
before do
- first('.done-todo').click
+ within first('.todo') do
+ click_link 'Done'
+ end
end
- it 'is removed from the list' do
- expect(page).not_to have_selector('.todos-list .todo')
+ it 'is marked as done-reversible in the list' do
+ expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible')
+ end
+
+ it 'shows Undo button' do
+ expect(page).to have_selector('.js-undo-todo', visible: true)
+ expect(page).to have_selector('.js-done-todo', visible: false)
+ end
+
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 1'
end
- it 'shows "All done" message' do
- expect(page).to have_selector('.todos-all-done', count: 1)
+ it 'has not "All done" message' do
+ expect(page).not_to have_selector('.todos-all-done')
end
end
+ shared_examples 'deleting and restoring the todo' do
+ before do
+ 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
+ expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible')
+ expect(page).to have_selector('.todos-list .todo.todo-pending')
+ end
+
+ it 'shows Done button' do
+ expect(page).to have_selector('.js-undo-todo', visible: false)
+ expect(page).to have_selector('.js-done-todo', visible: true)
+ end
+
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Done 0'
+ end
+ end
+
+ it_behaves_like 'deleting the todo'
+ it_behaves_like 'deleting and restoring the todo'
+
context 'todo is stale on the page' do
before do
todos = TodosFinder.new(user, state: :pending).execute
TodoService.new.mark_todos_as_done(todos, user)
end
- describe 'deleting the todo' do
- before do
- first('.done-todo').click
- end
+ it_behaves_like 'deleting the todo'
+ it_behaves_like 'deleting and restoring the todo'
+ end
+ end
- it 'is removed from the list' do
- expect(page).not_to have_selector('.todos-list .todo')
- 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 'shows "All done" message' do
- expect(page).to have_selector('.todos-all-done', count: 1)
+ 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
@@ -113,22 +173,10 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- describe 'completing last todo from last page', js: true do
- it 'redirects to the previous page' do
- visit dashboard_todos_path(page: 2)
- expect(page).to have_css("#todo_#{Todo.last.id}")
-
- click_link('Done')
-
- expect(current_path).to eq dashboard_todos_path
- expect(page).to have_css("#todo_#{Todo.first.id}")
- end
- end
-
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
@@ -156,6 +204,29 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User have large number of todos' do
+ before do
+ create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows 99+ for count >= 100 in notification' do
+ expect(page).to have_selector('.todos-pending-count', text: '99+')
+ end
+
+ it 'shows exact number in To do tab' do
+ expect(page).to have_selector('.todos-pending .badge', text: '101')
+ end
+
+ it 'shows exact number for count < 100' do
+ 3.times { first('.js-done-todo').click }
+
+ expect(page).to have_selector('.todos-pending-count', text: '98')
+ end
+ end
+
context 'User has a Build Failed todo' do
let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) }
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 72354834c5a..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]
- visit namespace_project_triggers_path(@project.namespace, @project)
+ @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/features/variables_spec.rb b/spec/features/variables_spec.rb
index ff30ffd7820..a362d6fd3b6 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -3,14 +3,14 @@ require 'spec_helper'
describe 'Project variables', js: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:variable) { create(:ci_variable, key: 'test') }
+ let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
login_as(user)
project.team << [user, :master]
project.variables << variable
- visit namespace_project_variables_path(project.namespace, project)
+ visit namespace_project_settings_ci_cd_path(project.namespace, project)
end
it 'shows list of variables' do
@@ -24,11 +24,23 @@ describe 'Project variables', js: true do
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
+ expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do
expect(page).to have_content('key')
end
end
+ it 'adds empty variable' do
+ fill_in('variable_key', with: 'new_key')
+ fill_in('variable_value', with: '')
+ click_button('Add new variable')
+
+ expect(page).to have_content('Variables were successfully updated.')
+ page.within('.variables-table') do
+ expect(page).to have_content('new_key')
+ end
+ end
+
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
@@ -72,8 +84,20 @@ describe 'Project variables', js: true do
fill_in('variable_value', with: 'key value')
click_button('Save variable')
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables.first.value).to eq('key value')
+ end
+
+ it 'edits variable with empty value' do
page.within('.variables-table') do
- expect(page).to have_content('key')
+ find('.btn-variable-edit').click
end
+
+ expect(page).to have_content('Update variable')
+ fill_in('variable_value', with: '')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables.first.value).to eq('')
end
end
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index ad2d456529a..34f665826b6 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -10,15 +10,12 @@ describe ContributedProjectsFinder do
let!(:private_project) { create(:empty_project, :private) }
before do
- private_project.team << [source_user, Gitlab::Access::MASTER]
- private_project.team << [current_user, Gitlab::Access::DEVELOPER]
- public_project.team << [source_user, Gitlab::Access::MASTER]
+ private_project.add_master(source_user)
+ private_project.add_developer(current_user)
+ public_project.add_master(source_user)
- create(:event, action: Event::PUSHED, project: public_project,
- target: public_project, author: source_user)
-
- create(:event, action: Event::PUSHED, project: private_project,
- target: private_project, author: source_user)
+ create(:event, :pushed, project: public_project, target: public_project, author: source_user)
+ create(:event, :pushed, project: private_project, target: private_project, author: source_user)
end
describe 'without a current user' do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
new file mode 100644
index 00000000000..0c063f6d5ee
--- /dev/null
+++ b/spec/finders/environments_finder_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe EnvironmentsFinder do
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:environment) { create(:environment, project: project) }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(described_class.new(project, user, ref: 'master').execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'commit deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment' do
+ expect(described_class.new(project, user, commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'recently updated' do
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it 'finds recently updated environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it 'does not find environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to be_empty
+ end
+ end
+
+ context 'when there are two environments that deploy to the same branch' do
+ let(:second_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: second_environment, ref: 'feature')
+ end
+
+ it 'finds both environments' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment, second_environment)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
new file mode 100644
index 00000000000..b762756f9ce
--- /dev/null
+++ b/spec/finders/group_members_finder_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe GroupMembersFinder, '#execute' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, :access_requestable, parent: group) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:user4) { create(:user) }
+
+ it 'returns members for top-level group' do
+ member1 = group.add_master(user1)
+ member2 = group.add_master(user2)
+ member3 = group.add_master(user3)
+
+ result = described_class.new(group).execute
+
+ expect(result.to_a).to eq([member3, member2, member1])
+ end
+
+ it 'returns members for nested group' do
+ group.add_master(user2)
+ nested_group.request_access(user4)
+ member1 = group.add_master(user1)
+ member3 = nested_group.add_master(user2)
+ member4 = nested_group.add_master(user3)
+
+ result = described_class.new(nested_group).execute
+
+ expect(result.to_a).to eq([member4, member3, member1])
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 97737d7ddc7..2a008427478 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -136,10 +136,10 @@ describe IssuesFinder do
end
end
- context 'filtering by issue iid' do
- let(:params) { { search: issue3.to_reference } }
+ context 'filtering by issues iids' do
+ let(:params) { { iids: issue3.iid } }
- it 'returns issue with iid match' do
+ it 'returns issues with iids match' do
expect(issues).to contain_exactly(issue3)
end
end
@@ -224,7 +224,7 @@ describe IssuesFinder do
let(:scope) { nil }
it "doesn't return team-only issues to non team members" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
issue = create(:issue, project: project)
expect(issues).not_to include(issue)
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/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 3dcd7781e5b..21ef94ac5d1 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -38,5 +38,13 @@ describe MergeRequestsFinder do
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(3)
end
+
+ it 'filters by iid' do
+ params = { project_id: project1.id, iids: merge_request1.iid }
+
+ merge_requests = MergeRequestsFinder.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1)
+ end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index bac653ea451..77a04507be1 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -9,8 +9,6 @@ describe NotesFinder do
end
describe '#execute' do
- it 'finds notes on snippets when project is public and user isnt a member'
-
it 'finds notes on merge requests' do
create(:note_on_merge_request, project: project)
@@ -45,9 +43,11 @@ describe NotesFinder do
context 'on restricted projects' do
let(:project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE,
- snippets_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ create(:empty_project,
+ :public,
+ :issues_private,
+ :snippets_private,
+ :merge_requests_private)
end
it 'publicly excludes notes on merge requests' do
@@ -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 77f2bcee1f3..21c078e0f44 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -6,10 +6,12 @@
"confidential"
],
"properties" : {
+ "id": { "type": "integer" },
"iid": { "type": "integer" },
"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/list.json b/spec/fixtures/api/schemas/list.json
index 8d94cf26ecb..819287bf919 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -10,7 +10,7 @@
"id": { "type": "integer" },
"list_type": {
"type": "string",
- "enum": ["backlog", "label", "done"]
+ "enum": ["label", "done"]
},
"label": {
"type": ["object", "null"],
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/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
new file mode 100644
index 00000000000..6181b3ccc86
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -0,0 +1,36 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external",
+ "private_token"
+ ],
+ "properties": {
+ "$ref": "full.json",
+ "private_token": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
new file mode 100644
index 00000000000..5587cfec61a
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
@@ -0,0 +1,77 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "email",
+ "name",
+ "state",
+ "avatar_url",
+ "web_url",
+ "created_at",
+ "is_admin",
+ "bio",
+ "location",
+ "skype",
+ "linkedin",
+ "twitter",
+ "website_url",
+ "organization",
+ "last_sign_in_at",
+ "confirmed_at",
+ "color_scheme_id",
+ "projects_limit",
+ "current_sign_in_at",
+ "identities",
+ "can_create_group",
+ "can_create_project",
+ "two_factor_enabled",
+ "external"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "username": { "type": "string" },
+ "email": {
+ "type": "string",
+ "pattern": "^[^@]+@[^@]+$"
+ },
+ "name": { "type": "string" },
+ "state": {
+ "type": "string",
+ "enum": ["active", "blocked"]
+ },
+ "avatar_url": { "type": "string" },
+ "web_url": { "type": "string" },
+ "created_at": { "type": "date" },
+ "is_admin": { "type": "boolean" },
+ "bio": { "type": ["string", "null"] },
+ "location": { "type": ["string", "null"] },
+ "skype": { "type": "string" },
+ "linkedin": { "type": "string" },
+ "twitter": { "type": "string "},
+ "website_url": { "type": "string" },
+ "organization": { "type": ["string", "null"] },
+ "last_sign_in_at": { "type": "date" },
+ "confirmed_at": { "type": ["date", "null"] },
+ "color_scheme_id": { "type": "integer" },
+ "projects_limit": { "type": "integer" },
+ "current_sign_in_at": { "type": "date" },
+ "identities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": ["github", "bitbucket", "google_oauth2"]
+ },
+ "extern_uid": { "type": ["number", "string"] }
+ }
+ }
+ },
+ "can_create_group": { "type": "boolean" },
+ "can_create_project": { "type": "boolean" },
+ "two_factor_enabled": { "type": "boolean" },
+ "external": { "type": "boolean" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json
deleted file mode 100644
index e6c1d9c9d84..00000000000
--- a/spec/fixtures/api/schemas/user/login.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "type": "object",
- "required": [
- "id",
- "username",
- "email",
- "name",
- "state",
- "avatar_url",
- "web_url",
- "created_at",
- "is_admin",
- "bio",
- "location",
- "skype",
- "linkedin",
- "twitter",
- "website_url",
- "organization",
- "last_sign_in_at",
- "confirmed_at",
- "theme_id",
- "color_scheme_id",
- "projects_limit",
- "current_sign_in_at",
- "identities",
- "can_create_group",
- "can_create_project",
- "two_factor_enabled",
- "external",
- "private_token"
- ],
- "properties": {
- "$ref": "full.json",
- "private_token": { "type": "string" }
- }
-}
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json
deleted file mode 100644
index dbd5d32e89c..00000000000
--- a/spec/fixtures/api/schemas/user/public.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "type": "object",
- "required": [
- "id",
- "username",
- "email",
- "name",
- "state",
- "avatar_url",
- "web_url",
- "created_at",
- "is_admin",
- "bio",
- "location",
- "skype",
- "linkedin",
- "twitter",
- "website_url",
- "organization",
- "last_sign_in_at",
- "confirmed_at",
- "theme_id",
- "color_scheme_id",
- "projects_limit",
- "current_sign_in_at",
- "identities",
- "can_create_group",
- "can_create_project",
- "two_factor_enabled",
- "external"
- ],
- "properties": {
- "id": { "type": "integer" },
- "username": { "type": "string" },
- "email": {
- "type": "string",
- "pattern": "^[^@]+@[^@]+$"
- },
- "name": { "type": "string" },
- "state": {
- "type": "string",
- "enum": ["active", "blocked"]
- },
- "avatar_url": { "type": "string" },
- "web_url": { "type": "string" },
- "created_at": { "type": "date" },
- "is_admin": { "type": "boolean" },
- "bio": { "type": ["string", "null"] },
- "location": { "type": ["string", "null"] },
- "skype": { "type": "string" },
- "linkedin": { "type": "string" },
- "twitter": { "type": "string "},
- "website_url": { "type": "string" },
- "organization": { "type": ["string", "null"] },
- "last_sign_in_at": { "type": "date" },
- "confirmed_at": { "type": ["date", "null"] },
- "theme_id": { "type": "integer" },
- "color_scheme_id": { "type": "integer" },
- "projects_limit": { "type": "integer" },
- "current_sign_in_at": { "type": "date" },
- "identities": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "provider": {
- "type": "string",
- "enum": ["github", "bitbucket", "google_oauth2"]
- },
- "extern_uid": { "type": ["number", "string"] }
- }
- }
- },
- "can_create_group": { "type": "boolean" },
- "can_create_project": { "type": "boolean" },
- "two_factor_enabled": { "type": "boolean" },
- "external": { "type": "boolean" }
- }
-}
diff --git a/spec/fixtures/mail_room_disabled.yml b/spec/fixtures/config/mail_room_disabled.yml
index 97f8cff051f..97f8cff051f 100644
--- a/spec/fixtures/mail_room_disabled.yml
+++ b/spec/fixtures/config/mail_room_disabled.yml
diff --git a/spec/fixtures/mail_room_enabled.yml b/spec/fixtures/config/mail_room_enabled.yml
index 9c94649244d..9c94649244d 100644
--- a/spec/fixtures/mail_room_enabled.yml
+++ b/spec/fixtures/config/mail_room_enabled.yml
diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
new file mode 100644
index 00000000000..6823db0cfc8
--- /dev/null
+++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
@@ -0,0 +1,42 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>,<exchange@microsoft.com>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
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/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz
new file mode 100644
index 00000000000..d0e89378b3e
--- /dev/null
+++ b/spec/fixtures/pages.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip
new file mode 100644
index 00000000000..9558fcd4b94
--- /dev/null
+++ b/spec/fixtures/pages.zip
Binary files differ
diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta
new file mode 100644
index 00000000000..1e6198a15f0
--- /dev/null
+++ b/spec/fixtures/pages.zip.meta
Binary files differ
diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz
new file mode 100644
index 00000000000..5c2afa1a8f6
--- /dev/null
+++ b/spec/fixtures/pages_empty.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip
new file mode 100644
index 00000000000..db3f0334c12
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta
new file mode 100644
index 00000000000..d0b93b3b9c0
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip.meta
Binary files differ
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8b201f348f1..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
@@ -265,4 +265,9 @@ describe ApplicationHelper do
expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
end
end
+
+ describe '#active_when' do
+ it { expect(helper.active_when(true)).to eq('active') }
+ it { expect(helper.active_when(false)).to eq(nil) }
+ end
end
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/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 727c25ff529..a2c008790f9 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -26,4 +26,23 @@ describe CommitsHelper do
not_to include('onmouseover="alert(1)"')
end
end
+
+ describe '#view_on_environment_button' do
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, external_url: 'http://example.com') }
+ let(:path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html')
+ end
+
+ it 'returns a link tag linking to the file in the environment' do
+ html = helper.view_on_environment_button(sha, path, environment)
+ node = Nokogiri::HTML.parse(html).at_css('a')
+
+ expect(node[:title]).to eq('View on example.com')
+ expect(node[:href]).to eq('http://example.com/file.html')
+ 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..81ba693f2f3 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -61,6 +61,13 @@ describe EventsHelper do
'</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
describe '#event_commit_title' do
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index b8ec3521edb..9ffd4b9371c 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
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/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 550b4a66a6a..25f23826648 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -63,9 +63,11 @@ describe MergeRequestsHelper do
end
end
- describe 'mr_widget_refresh_url' do
- let(:project) { create(:empty_project) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ describe '#mr_widget_refresh_url' do
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:project_fork) { Projects::ForkService.new(project, guest).execute }
+ let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
it 'returns correct url for MR' do
expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
@@ -74,7 +76,89 @@ describe MergeRequestsHelper do
end
it 'returns empty string for nil' do
- expect(mr_widget_refresh_url(nil)).to end_with('')
+ expect(mr_widget_refresh_url(nil)).to eq('')
+ end
+ end
+
+ describe '#mr_closes_issues' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1, issue_2)
+ end
+ end
+ end
+
+ describe '#mr_issues_mentioned_but_not_closing' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2)
+ end
end
end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 14a95479339..68b20a1e4fc 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
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index dc07657e101..2cc0b40b2d0 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -40,6 +40,18 @@ describe PageLayoutHelper do
end
end
+ describe 'favicon' do
+ it 'defaults to favicon.ico' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ expect(helper.favicon).to eq 'favicon.ico'
+ end
+
+ it 'has blue favicon for development' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ expect(helper.favicon).to eq 'favicon-blue.ico'
+ end
+ end
+
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 1f02e06e312..f3e79cc7290 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -26,32 +26,6 @@ describe PreferencesHelper do
end
end
- describe 'user_application_theme' do
- context 'with a user' do
- it "returns user's theme's css_class" do
- stub_user(theme_id: 3)
-
- expect(helper.user_application_theme).to eq 'ui_green'
- end
-
- it 'returns the default when id is invalid' do
- stub_user(theme_id: Gitlab::Themes.count + 5)
-
- allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2)
-
- expect(helper.user_application_theme).to eq 'ui_charcoal'
- end
- end
-
- context 'without a user' do
- it 'returns the default theme' do
- stub_user
-
- expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
- end
- end
- end
-
describe 'user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 8d1570aa6f3..aca0bb1d794 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -203,7 +203,6 @@ describe ProjectsHelper do
context "when project moves from public to private" do
before do
- project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
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/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/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
new file mode 100644
index 00000000000..92c6f27a867
--- /dev/null
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe WikiHelper do
+ describe '#breadcrumb' do
+ context 'when the page is at the root level' do
+ it 'returns the capitalized page name' do
+ slug = 'page-name'
+
+ expect(helper.breadcrumb(slug)).to eq('Page name')
+ end
+ end
+
+ context 'when the page is inside a directory' do
+ it 'returns the capitalized name of each directory and of the page itself' do
+ slug = 'dir_1/page-name'
+
+ expect(helper.breadcrumb(slug)).to eq('Dir_1 / Page name')
+ 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 3cd419b37c9..3d922021978 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,13 +18,15 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
- "spyOnEvent": false
+ "spyOnEvent": false,
+ "ClassSpecHelper": false
},
"plugins": ["jasmine"],
"rules": {
- "prefer-arrow-callback": 0,
"func-names": 0,
"jasmine/no-suite-dupes": [1, "branch"],
- "jasmine/no-spec-dupes": [1, "branch"]
+ "jasmine/no-spec-dupes": [1, "branch"],
+ "no-console": 0,
+ "prefer-arrow-callback": 0
}
}
diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js
new file mode 100644
index 00000000000..76b370b345b
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js
@@ -0,0 +1,43 @@
+require('~/lib/utils/text_utility');
+require('~/abuse_reports');
+
+((global) => {
+ describe('Abuse Reports', () => {
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ let messages;
+
+ const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ const findMessage = searchText => messages.filter(
+ (index, element) => element.innerText.indexOf(searchText) > -1,
+ ).first();
+
+ preloadFixtures(FIXTURE);
+
+ beforeEach(function () {
+ loadFixtures(FIXTURE);
+ this.abuseReports = new global.AbuseReports();
+ messages = $('.abuse-reports .message');
+ });
+
+ it('should truncate long messages', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', () => {
+ const $shortMessage = findMessage('SHORT MESSAGE');
+ expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
+ });
+
+ it('should allow clicking a truncated message to expand and collapse the full message', () => {
+ const $longMessage = findMessage('LONG MESSAGE');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
+ });
+ });
+})(window.gl);
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
deleted file mode 100644
index a2d57824585..00000000000
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ /dev/null
@@ -1,43 +0,0 @@
-/*= require lib/utils/text_utility */
-/*= require abuse_reports */
-
-((global) => {
- describe('Abuse Reports', () => {
- const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
- const MAX_MESSAGE_LENGTH = 500;
-
- let messages;
-
- const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- const findMessage = searchText => messages.filter(
- (index, element) => element.innerText.indexOf(searchText) > -1,
- ).first();
-
- preloadFixtures(FIXTURE);
-
- beforeEach(function () {
- loadFixtures(FIXTURE);
- this.abuseReports = new global.AbuseReports();
- messages = $('.abuse-reports .message');
- });
-
- it('should truncate long messages', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('original-message')).toEqual(jasmine.anything());
- assertMaxLength($longMessage);
- });
-
- it('should not truncate short messages', () => {
- const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
- });
-
- it('should allow clicking a truncated message to expand and collapse the full message', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- $longMessage.click();
- expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
- $longMessage.click();
- assertMaxLength($longMessage);
- });
- });
-})(window.gl);
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
new file mode 100644
index 00000000000..e6a6fc36ca1
--- /dev/null
+++ b/spec/javascripts/activities_spec.js
@@ -0,0 +1,62 @@
+/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
+
+require('vendor/jquery.endless-scroll.js');
+require('~/pager');
+require('~/activities');
+
+(() => {
+ window.gon || (window.gon = {});
+ const fixtureTemplate = 'static/event_filter.html.raw';
+ const filters = [
+ {
+ id: 'all',
+ }, {
+ id: 'push',
+ name: 'push events',
+ }, {
+ id: 'merged',
+ name: 'merge events',
+ }, {
+ id: 'comments',
+ }, {
+ id: 'team',
+ }];
+
+ function getEventName(index) {
+ const filter = filters[index];
+ return filter.hasOwnProperty('name') ? filter.name : filter.id;
+ }
+
+ function getSelector(index) {
+ const filter = filters[index];
+ return `#${filter.id}_event_filter`;
+ }
+
+ describe('Activities', () => {
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ new gl.Activities();
+ });
+
+ for (let i = 0; i < filters.length; i += 1) {
+ ((i) => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for (let x = 0; x < filters.length; x += 1) {
+ ((x) => {
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+ });
+})();
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
deleted file mode 100644
index 7bc5b3268a0..00000000000
--- a/spec/javascripts/activities_spec.js.es6
+++ /dev/null
@@ -1,63 +0,0 @@
-/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
-
-/*= require js.cookie.js */
-/*= require jquery.endless-scroll.js */
-/*= require pager */
-/*= require activities */
-
-(() => {
- window.gon || (window.gon = {});
- const fixtureTemplate = 'static/event_filter.html.raw';
- const filters = [
- {
- id: 'all',
- }, {
- id: 'push',
- name: 'push events',
- }, {
- id: 'merged',
- name: 'merge events',
- }, {
- id: 'comments',
- }, {
- id: 'team',
- }];
-
- function getEventName(index) {
- const filter = filters[index];
- return filter.hasOwnProperty('name') ? filter.name : filter.id;
- }
-
- function getSelector(index) {
- const filter = filters[index];
- return `#${filter.id}_event_filter`;
- }
-
- describe('Activities', () => {
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new gl.Activities();
- });
-
- for (let i = 0; i < filters.length; i += 1) {
- ((i) => {
- describe(`when selecting ${getEventName(i)}`, () => {
- beforeEach(() => {
- $(getSelector(i)).click();
- });
-
- for (let x = 0; x < filters.length; x += 1) {
- ((x) => {
- const shouldHighlight = i === x;
- const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
-
- it(`${testName} ${getEventName(x)}`, () => {
- expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
- });
- })(x);
- }
- });
- })(i);
- }
- });
-})();
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 f1bfd529983..0a6e042b700 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,13 +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 jquery */
-/*= require js.cookie */
-/*= 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;
@@ -15,14 +12,6 @@
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) {
@@ -34,22 +23,37 @@
};
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) {
@@ -64,10 +68,9 @@
});
});
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);
});
});
@@ -88,7 +91,7 @@
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);
@@ -98,14 +101,14 @@
$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);
@@ -122,8 +125,8 @@
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);
@@ -140,9 +143,9 @@
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() {
@@ -150,7 +153,7 @@
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();
@@ -160,7 +163,7 @@
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();
@@ -172,7 +175,7 @@
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);
@@ -183,7 +186,7 @@
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);
@@ -192,43 +195,98 @@
});
});
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}`);
+ });
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 51d911792ba..3deaf258cae 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
-/*= require behaviors/autosize */
+require('~/behaviors/autosize');
(function() {
describe('Autosize behavior', function() {
@@ -15,7 +15,7 @@
});
});
return load = function() {
- return $(document).trigger('page:load');
+ return $(document).trigger('load');
};
});
-}).call(this);
+}).call(window);
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/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 0f046c2d83a..4820ce41ade 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,77 +1,87 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */
-/*= require behaviors/quick_submit */
+require('~/behaviors/quick_submit');
(function() {
describe('Quick Submit behavior', function() {
var keydownEvent;
- preloadFixtures('static/behaviors/quick_submit.html.raw');
+ preloadFixtures('issues/open-issue.html.raw');
beforeEach(function() {
- loadFixtures('static/behaviors/quick_submit.html.raw');
+ loadFixtures('issues/open-issue.html.raw');
$('form').submit(function(e) {
// Prevent a form submit from moving us off the testing page
return e.preventDefault();
});
- return this.spies = {
+ this.spies = {
submit: spyOnEvent('form', 'submit')
};
+
+ this.textarea = $('.js-quick-submit textarea').first();
});
it('does not respond to other keyCodes', function() {
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
keyCode: 32
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to Enter alone', function() {
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
ctrlKey: false,
metaKey: false
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
it('does not respond to repeated events', function() {
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
repeat: true
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
- it('disables submit buttons', function() {
- $('textarea').trigger(keydownEvent());
- expect($('input[type=submit]')).toBeDisabled();
- return expect($('button[type=submit]')).toBeDisabled();
+ it('disables input of type submit', function() {
+ const submitButton = $('.js-quick-submit input[type=submit]');
+ this.textarea.trigger(keydownEvent());
+ expect(submitButton).toBeDisabled();
+ });
+ it('disables button of type submit', function() {
+ // button doesn't exist in fixture, add it manually
+ const submitButton = $('<button type="submit">Submit it</button>');
+ submitButton.insertAfter(this.textarea);
+
+ this.textarea.trigger(keydownEvent());
+ expect(submitButton).toBeDisabled();
});
- // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
+ // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
// only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) {
it('responds to Meta+Enter', function() {
- $('input.quick-submit-input').trigger(keydownEvent());
+ this.textarea.trigger(keydownEvent());
return expect(this.spies.submit).toHaveBeenTriggered();
});
it('excludes other modifier keys', function() {
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
altKey: true
}));
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
ctrlKey: true
}));
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
shiftKey: true
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
} else {
it('responds to Ctrl+Enter', function() {
- $('input.quick-submit-input').trigger(keydownEvent());
+ this.textarea.trigger(keydownEvent());
return expect(this.spies.submit).toHaveBeenTriggered();
});
it('excludes other modifier keys', function() {
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
altKey: true
}));
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
metaKey: true
}));
- $('input.quick-submit-input').trigger(keydownEvent({
+ this.textarea.trigger(keydownEvent({
shiftKey: true
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
@@ -93,4 +103,4 @@
return $.Event('keydown', $.extend({}, defaults, options));
};
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 9467056f04c..3a84013a2ed 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,21 +1,22 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require behaviors/requires_input */
+require('~/behaviors/requires_input');
(function() {
describe('requiresInput', function() {
- preloadFixtures('static/behaviors/requires_input.html.raw');
+ preloadFixtures('branches/new_branch.html.raw');
beforeEach(function() {
- return loadFixtures('static/behaviors/requires_input.html.raw');
+ loadFixtures('branches/new_branch.html.raw');
+ this.submitButton = $('button[type="submit"]');
});
it('disables submit when any field is required', function() {
$('.js-requires-input').requiresInput();
- return expect($('.submit')).toBeDisabled();
+ return expect(this.submitButton).toBeDisabled();
});
it('enables submit when no field is required', function() {
$('*[required=required]').removeAttr('required');
$('.js-requires-input').requiresInput();
- return expect($('.submit')).not.toBeDisabled();
+ return expect(this.submitButton).not.toBeDisabled();
});
it('enables submit when all required fields are pre-filled', function() {
$('*[required=required]').remove();
@@ -25,20 +26,14 @@
it('enables submit when all required fields receive input', function() {
$('.js-requires-input').requiresInput();
$('#required1').val('input1').change();
- expect($('.submit')).toBeDisabled();
+ expect(this.submitButton).toBeDisabled();
$('#optional1').val('input1').change();
- expect($('.submit')).toBeDisabled();
+ expect(this.submitButton).toBeDisabled();
$('#required2').val('input2').change();
$('#required3').val('input3').change();
$('#required4').val('input4').change();
$('#required5').val('1').change();
return expect($('.submit')).not.toBeDisabled();
});
- return it('is called on page:load event', function() {
- var spy;
- spy = spyOn($.fn, 'requiresInput');
- $(document).trigger('page:load');
- return expect(spy).toHaveBeenCalled();
- });
});
-}).call(this);
+}).call(window);
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_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 7c5850111cb..00000000000
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ /dev/null
@@ -1,178 +0,0 @@
-/* eslint-disable comma-dangle, one-var, no-unused-vars */
-/* global Vue */
-/* global BoardService */
-/* global boardsMockInterceptor */
-/* global Cookies */
-/* global listObj */
-/* global listObjDuplicate */
-
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= 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('finds list limited by type', () => {
- gl.issueBoards.BoardsStore.addList({
- id: 1,
- position: 0,
- title: 'Test',
- list_type: 'backlog'
- });
- const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
-
- 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 backlog & done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'backlog'
- });
- 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 b/spec/javascripts/boards/issue_card_spec.js
new file mode 100644
index 00000000000..4340a571017
--- /dev/null
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -0,0 +1,191 @@
+/* global Vue */
+/* global ListUser */
+/* global ListLabel */
+/* global listObj */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/boards_store');
+require('~/boards/components/issue_card_inner');
+require('./mock_data');
+
+describe('Issue card component', () => {
+ const user = new ListUser({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+ let component;
+ let issue;
+ let list;
+
+ beforeEach(() => {
+ setFixtures('<div class="test-container"></div>');
+
+ list = listObj;
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [list.label],
+ });
+
+ component = new Vue({
+ el: document.querySelector('.test-container'),
+ data() {
+ return {
+ list,
+ issue,
+ issueLinkBase: '/test',
+ rootPath: '/',
+ };
+ },
+ components: {
+ 'issue-card': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <issue-card
+ :issue="issue"
+ :list="list"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"></issue-card>
+ `,
+ });
+ });
+
+ it('renders issue title', () => {
+ expect(
+ component.$el.querySelector('.card-title').textContent,
+ ).toContain(issue.title);
+ });
+
+ it('includes issue base in link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('href'),
+ ).toContain('/test');
+ });
+
+ it('includes issue title on link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('title'),
+ ).toBe(issue.title);
+ });
+
+ it('does not render confidential icon', () => {
+ expect(
+ component.$el.querySelector('.fa-eye-flash'),
+ ).toBeNull();
+ });
+
+ it('renders confidential icon', (done) => {
+ component.issue.confidential = true;
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.confidential-icon'),
+ ).not.toBeNull();
+ done();
+ }, 0);
+ });
+
+ it('renders issue ID with #', () => {
+ expect(
+ component.$el.querySelector('.card-number').textContent,
+ ).toContain(`#${issue.id}`);
+ });
+
+ describe('assignee', () => {
+ it('does not render assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.assignee = user;
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('renders assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).not.toBeNull();
+ });
+
+ it('sets title', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('title'),
+ ).toContain(`Assigned to ${user.name}`);
+ });
+
+ it('sets users path', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('href'),
+ ).toBe('/test');
+ });
+
+ it('renders avatar', () => {
+ expect(
+ component.$el.querySelector('.card-assignee img'),
+ ).not.toBeNull();
+ });
+ });
+ });
+
+ describe('labels', () => {
+ it('does not render any', () => {
+ expect(
+ component.$el.querySelector('.label'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.addLabel(label1);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('does not render list label', () => {
+ expect(
+ component.$el.querySelectorAll('.label').length,
+ ).toBe(1);
+ });
+
+ it('renders label', () => {
+ expect(
+ component.$el.querySelector('.label').textContent,
+ ).toContain(label1.title);
+ });
+
+ it('sets label description as title', () => {
+ expect(
+ component.$el.querySelector('.label').getAttribute('title'),
+ ).toContain(label1.description);
+ });
+
+ it('sets background color of button', () => {
+ expect(
+ component.$el.querySelector('.label').style.backgroundColor,
+ ).toContain(label1.color);
+ });
+ });
+ });
+});
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 c8a61a0a9b5..00000000000
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ /dev/null
@@ -1,87 +0,0 @@
-/* eslint-disable comma-dangle */
-/* global BoardService */
-/* global ListIssue */
-
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= 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 7d942ec3d65..00000000000
--- a/spec/javascripts/boards/list_spec.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable comma-dangle */
-/* global Vue */
-/* global boardsMockInterceptor */
-/* global BoardService */
-/* global List */
-/* global listObj */
-
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= 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 b/spec/javascripts/boards/mock_data.js
new file mode 100644
index 00000000000..7a399b307ad
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js
@@ -0,0 +1,63 @@
+/* eslint-disable comma-dangle, no-unused-vars, quote-props */
+
+const listObj = {
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const listObjDuplicate = {
+ id: 2,
+ position: 1,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 2,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const BoardsMockData = {
+ 'GET': {
+ '/test/issue-boards/board/1/lists{/id}/issues': {
+ issues: [{
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: []
+ }],
+ size: 1
+ }
+ },
+ 'POST': {
+ '/test/issue-boards/board/1/lists{/id}': listObj
+ },
+ 'PUT': {
+ '/test/issue-boards/board/1/lists{/id}': {}
+ },
+ 'DELETE': {
+ '/test/issue-boards/board/1/lists{/id}': {}
+ }
+};
+
+const boardsMockInterceptor = (request, next) => {
+ const body = BoardsMockData[request.method][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200
+ }));
+};
+
+window.listObj = listObj;
+window.listObjDuplicate = listObjDuplicate;
+window.BoardsMockData = BoardsMockData;
+window.boardsMockInterceptor = boardsMockInterceptor;
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
deleted file mode 100644
index 8d3e2237fda..00000000000
--- a/spec/javascripts/boards/mock_data.js.es6
+++ /dev/null
@@ -1,58 +0,0 @@
-/* eslint-disable comma-dangle, no-unused-vars, quote-props */
-
-const listObj = {
- id: 1,
- position: 0,
- title: 'Test',
- list_type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;'
- }
-};
-
-const listObjDuplicate = {
- id: 2,
- position: 1,
- title: 'Test',
- list_type: 'label',
- label: {
- id: 2,
- title: 'Testing',
- color: 'red',
- description: 'testing;'
- }
-};
-
-const BoardsMockData = {
- 'GET': {
- '/test/issue-boards/board/1/lists{/id}/issues': {
- issues: [{
- title: 'Testing',
- iid: 1,
- confidential: false,
- labels: []
- }],
- size: 1
- }
- },
- 'POST': {
- '/test/issue-boards/board/1/lists{/id}': listObj
- },
- 'PUT': {
- '/test/issue-boards/board/1/lists{/id}': {}
- },
- 'DELETE': {
- '/test/issue-boards/board/1/lists{/id}': {}
- }
-};
-
-const boardsMockInterceptor = (request, next) => {
- const body = BoardsMockData[request.method][request.url];
-
- next(request.respondWith(JSON.stringify(body), {
- status: 200
- }));
-};
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
new file mode 100644
index 00000000000..1815847f3fa
--- /dev/null
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -0,0 +1,132 @@
+/* global Vue */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/modal_store');
+
+describe('Modal store', () => {
+ let issue;
+ let issue2;
+ const Store = gl.issueBoards.ModalStore;
+
+ beforeEach(() => {
+ // Setup default state
+ Store.store.issues = [];
+ Store.store.selectedIssues = [];
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [],
+ });
+ issue2 = new ListIssue({
+ title: 'Testing',
+ iid: 2,
+ confidential: false,
+ labels: [],
+ });
+ Store.store.issues.push(issue);
+ Store.store.issues.push(issue2);
+ });
+
+ it('returns selected count', () => {
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles the issue as selected', () => {
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('toggles the issue as un-selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all issues as selected', () => {
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('toggles all issues as un-selected', () => {
+ Store.toggleAll();
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(false);
+ expect(issue2.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all if a single issue is selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('adds issue to selected array', () => {
+ issue.selected = true;
+ Store.addSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('removes issue from selected array', () => {
+ Store.addSelectedIssue(issue);
+ Store.removeSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('returns selected issue index if present', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.selectedIssueIndex(issue)).toBe(0);
+ });
+
+ it('returns -1 if issue is not selected', () => {
+ expect(Store.selectedIssueIndex(issue)).toBe(-1);
+ });
+
+ it('finds the selected issue', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.findSelectedIssue(issue)).toBe(issue);
+ });
+
+ it('does not find a selected issue', () => {
+ expect(Store.findSelectedIssue(issue)).toBe(undefined);
+ });
+
+ it('does not remove from selected issue if tab is not all', () => {
+ Store.store.activeTab = 'selected';
+
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(Store.store.selectedIssues.length).toBe(1);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('gets selected issue array with only selected issues', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue2);
+ Store.toggleIssue(issue2);
+
+ expect(Store.getSelectedIssues().length).toBe(1);
+ });
+});
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 b/spec/javascripts/bootstrap_linked_tabs_spec.js
new file mode 100644
index 00000000000..fa9f95e16cd
--- /dev/null
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js
@@ -0,0 +1,71 @@
+require('~/lib/utils/bootstrap_linked_tabs');
+
+(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
+ describe('Linked Tabs', () => {
+ preloadFixtures('static/linked_tabs.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/linked_tabs.html.raw');
+ });
+
+ describe('when is initialized', () => {
+ beforeEach(() => {
+ if (!phantomjs) {
+ spyOn(window.history, 'replaceState').and.callFake(function () {});
+ }
+ });
+
+ it('should activate the tab correspondent to the given action', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'tab1',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+
+ it('should active the default tab action when the action is show', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+ });
+
+ describe('on click', () => {
+ it('should change the url according to the clicked tab', () => {
+ const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
+
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
+ const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash;
+
+ secondTab.click();
+
+ if (historySpy) {
+ expect(historySpy).toHaveBeenCalledWith({
+ url: newState,
+ }, document.title, newState);
+ }
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
deleted file mode 100644
index ea953d0f5a5..00000000000
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
+++ /dev/null
@@ -1,59 +0,0 @@
-//= require lib/utils/bootstrap_linked_tabs
-
-(() => {
- describe('Linked Tabs', () => {
- preloadFixtures('static/linked_tabs.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/linked_tabs.html.raw');
- });
-
- describe('when is initialized', () => {
- beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function () {});
- });
-
- it('should activate the tab correspondent to the given action', () => {
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
- action: 'tab1',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- expect(document.querySelector('#tab1').classList).toContain('active');
- });
-
- it('should active the default tab action when the action is show', () => {
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
- action: 'show',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- expect(document.querySelector('#tab1').classList).toContain('active');
- });
- });
-
- describe('on click', () => {
- it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
-
- const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
- action: 'show',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
- const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash;
-
- secondTab.click();
-
- expect(historySpy).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState,
- }, document.title, newState);
- });
- });
- });
-})();
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
new file mode 100644
index 00000000000..fe7f3d2e9c4
--- /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('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 0c556382980..00000000000
--- a/spec/javascripts/build_spec.js.es6
+++ /dev/null
@@ -1,184 +0,0 @@
-/* eslint-disable no-new */
-/* global Build */
-/* global Turbolinks */
-
-//= require lib/utils/datetime_utility
-//= require build
-//= require breakpoints
-//= require jquery.nicescroll
-//= require turbolinks
-
-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(Turbolinks, 'visit');
-
- jasmine.clock().tick(4001);
- const [{ success, context }] = $.ajax.calls.argsFor(1);
- success.call(context, {
- html: '<span>Final</span>',
- status: 'passed',
- append: true,
- });
-
- expect(Turbolinks.visit).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..188908d66bd
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -0,0 +1,92 @@
+/* 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..f09c57978a1
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -0,0 +1,105 @@
+/* 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 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js
new file mode 100644
index 00000000000..94973419979
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js
@@ -0,0 +1,33 @@
+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 b/spec/javascripts/commits_spec.js
new file mode 100644
index 00000000000..05260760c43
--- /dev/null
+++ b/spec/javascripts/commits_spec.js
@@ -0,0 +1,62 @@
+/* global CommitsList */
+
+require('vendor/jquery.endless-scroll');
+require('~/pager');
+require('~/commits');
+
+(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
+ describe('Commits List', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ });
+
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
+
+ describe('on entering input', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ CommitsList.init(25);
+ CommitsList.searchField.val('');
+
+ if (!phantomjs) {
+ spyOn(history, 'replaceState').and.stub();
+ }
+ ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
+ req.success({
+ data: '<li>Result</li>',
+ });
+ });
+ });
+
+ it('should save the last search string', () => {
+ CommitsList.searchField.val('GitLab');
+ CommitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+ });
+
+ it('should not make ajax call if the input does not change', () => {
+ CommitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6
deleted file mode 100644
index bb9a9072f3a..00000000000
--- a/spec/javascripts/commits_spec.js.es6
+++ /dev/null
@@ -1,50 +0,0 @@
-/* global CommitsList */
-
-//= require jquery.endless-scroll
-//= require pager
-//= require commits
-
-(() => {
- describe('Commits List', () => {
- beforeEach(() => {
- setFixtures(`
- <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
- <input id="commits-search">
- </form>
- <ol id="commits-list"></ol>
- `);
- });
-
- it('should be defined', () => {
- expect(CommitsList).toBeDefined();
- });
-
- describe('on entering input', () => {
- let ajaxSpy;
-
- beforeEach(() => {
- CommitsList.init(25);
- CommitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
- ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
- req.success({
- data: '<li>Result</li>',
- });
- });
- });
-
- it('should save the last search string', () => {
- CommitsList.searchField.val('GitLab');
- CommitsList.filterResults();
- expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
- });
-
- it('should not make ajax call if the input does not change', () => {
- CommitsList.filterResults();
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
- });
- });
- });
-})();
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
deleted file mode 100644
index 4d851b2d320..00000000000
--- a/spec/javascripts/dashboard_spec.js.es6
+++ /dev/null
@@ -1,39 +0,0 @@
-/* eslint-disable no-new */
-
-/*= require sidebar */
-/*= require jquery */
-/*= require js.cookie */
-/*= require lib/utils/text_utility */
-
-((global) => {
- describe('Dashboard', () => {
- const fixtureTemplate = 'static/dashboard.html.raw';
-
- function todosCountText() {
- return $('.js-todos-count').text();
- }
-
- function triggerToggle(newCount) {
- $(document).trigger('todo:toggle', newCount);
- }
-
- preloadFixtures(fixtureTemplate);
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new global.Sidebar();
- });
-
- it('should update todos-count after receiving the todo:toggle event', () => {
- triggerToggle(5);
- expect(todosCountText()).toEqual('5');
- });
-
- it('should display todos-count with delimiter', () => {
- triggerToggle(1000);
- expect(todosCountText()).toEqual('1,000');
-
- triggerToggle(1000000);
- expect(todosCountText()).toEqual('1,000,000');
- });
- });
-})(window.gl);
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
new file mode 100644
index 00000000000..d5eec10be42
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -0,0 +1,65 @@
+require('~/lib/utils/datetime_utility');
+
+(() => {
+ describe('Date time utils', () => {
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = gl.utils.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = gl.utils.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = gl.utils.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = gl.utils.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = gl.utils.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = gl.utils.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = gl.utils.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
+ });
+ });
+
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
+
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
+ });
+
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
deleted file mode 100644
index 8ece24555c5..00000000000
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ /dev/null
@@ -1,65 +0,0 @@
-//= require lib/utils/datetime_utility
-
-(() => {
- describe('Date time utils', () => {
- describe('get day name', () => {
- it('should return Sunday', () => {
- const day = gl.utils.getDayName(new Date('07/17/2016'));
- expect(day).toBe('Sunday');
- });
-
- it('should return Monday', () => {
- const day = gl.utils.getDayName(new Date('07/18/2016'));
- expect(day).toBe('Monday');
- });
-
- it('should return Tuesday', () => {
- const day = gl.utils.getDayName(new Date('07/19/2016'));
- expect(day).toBe('Tuesday');
- });
-
- it('should return Wednesday', () => {
- const day = gl.utils.getDayName(new Date('07/20/2016'));
- expect(day).toBe('Wednesday');
- });
-
- it('should return Thursday', () => {
- const day = gl.utils.getDayName(new Date('07/21/2016'));
- expect(day).toBe('Thursday');
- });
-
- it('should return Friday', () => {
- const day = gl.utils.getDayName(new Date('07/22/2016'));
- expect(day).toBe('Friday');
- });
-
- it('should return Saturday', () => {
- const day = gl.utils.getDayName(new Date('07/23/2016'));
- expect(day).toBe('Saturday');
- });
- });
-
- describe('get day difference', () => {
- it('should return 7', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('07/08/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(7);
- });
-
- it('should return 31', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('08/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(31);
- });
-
- it('should return 365', () => {
- const firstDay = new Date('07/02/2015');
- const secondDay = new Date('07/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(365);
- });
- });
- });
-})();
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 fbfa34a5da7..00000000000
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ /dev/null
@@ -1,125 +0,0 @@
-/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
-/* global CommentsStore */
-
-//= require vue
-//= 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 056e4d41e93..00000000000
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ /dev/null
@@ -1,67 +0,0 @@
-//= require vue
-//= 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 window.gl.environmentsList.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 window.gl.environmentsList.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 950a5d53fad..00000000000
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ /dev/null
@@ -1,22 +0,0 @@
-//= require vue
-//= 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 window.gl.environmentsList.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 c178b9cc1ec..00000000000
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ /dev/null
@@ -1,229 +0,0 @@
-//= require vue
-//= require timeago
-//= 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',
- children: [
- {
- name: 'review-app',
- id: 1,
- state: 'available',
- external_url: '',
- last_deployment: {},
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-10T15:55:58.778Z',
- },
- {
- name: 'production',
- id: 2,
- state: 'available',
- external_url: '',
- last_deployment: {},
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-10T15:55:58.778Z',
- },
- ],
- };
-
- component = new window.gl.environmentsList.EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
- propsData: {
- model: mockItem,
- toggleRow: () => {},
- 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.children.length);
- });
- });
-
- describe('when item is not folder', () => {
- let environment;
- let component;
-
- beforeEach(() => {
- environment = {
- id: 31,
- name: 'production',
- 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',
- },
- ],
- },
- 'stoppable?': 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 window.gl.environmentsList.EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
- propsData: {
- model: environment,
- toggleRow: () => {},
- 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 95796f23894..00000000000
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ /dev/null
@@ -1,47 +0,0 @@
-//= require vue
-//= 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 window.gl.environmentsList.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 window.gl.environmentsList.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 window.gl.environmentsList.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 20e11ca3738..00000000000
--- a/spec/javascripts/environments/environment_spec.js.es6
+++ /dev/null
@@ -1,127 +0,0 @@
-/* global Vue, environment */
-
-//= require vue
-//= require vue-resource
-//= require flash
-//= require environments/stores/environments_store
-//= require environments/components/environment
-//= require ./mock_data
-
-describe('Environment', () => {
- preloadFixtures('environments/environments');
-
- let component;
-
- beforeEach(() => {
- loadFixtures('environments/environments');
- });
-
- 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 gl.environmentsList.EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
- });
-
- 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 environments', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([environment]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- });
-
- it('should render a table with environments', (done) => {
- component = new gl.environmentsList.EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(1);
- 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 gl.environmentsList.EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
- });
-
- 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 bb998a32f32..00000000000
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ /dev/null
@@ -1,28 +0,0 @@
-//= require vue
-//= 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 window.gl.environmentsList.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_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 17c00acf63e..00000000000
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ /dev/null
@@ -1,71 +0,0 @@
-/* global environmentsList */
-
-//= require vue
-//= require environments/stores/environments_store
-//= require ./mock_data
-
-(() => {
- describe('Store', () => {
- beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.create();
- });
-
- it('should start with a blank state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0);
- expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0);
- expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0);
- });
-
- describe('store environments', () => {
- beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
- });
-
- it('should count stopped environments and save the count in the state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
- });
-
- it('should count available environments and save the count in the state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
- });
-
- it('should store environments with same environment_type as sibilings', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
-
- const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
- .filter(env => env.children && env.children.length > 0);
-
- expect(parentFolder[0].children.length).toBe(2);
- expect(parentFolder[0].children[0].environment_type).toBe('review');
- expect(parentFolder[0].children[1].environment_type).toBe('review');
- expect(parentFolder[0].children[0].name).toBe('test-environment');
- expect(parentFolder[0].children[1].name).toBe('test-environment-1');
- });
-
- it('should sort the environments alphabetically', () => {
- const { environments } = gl.environmentsList.EnvironmentsStore.state;
-
- expect(environments[0].name).toBe('production');
- expect(environments[1].name).toBe('review');
- expect(environments[1].children[0].name).toBe('test-environment');
- expect(environments[1].children[1].name).toBe('test-environment-1');
- expect(environments[2].name).toBe('review_app');
- });
- });
-
- describe('toggleFolder', () => {
- beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
- });
-
- it('should toggle the open property for the given environment', () => {
- gl.environmentsList.EnvironmentsStore.toggleFolder('review');
-
- const { environments } = gl.environmentsList.EnvironmentsStore.state;
- const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
-
- expect(environment[0].isOpen).toBe(true);
- });
- });
- });
-})();
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/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 8ecd01f9a83..00000000000
--- a/spec/javascripts/environments/mock_data.js.es6
+++ /dev/null
@@ -1,149 +0,0 @@
-/* eslint-disable no-unused-vars */
-const environmentsList = [
- {
- id: 31,
- name: 'production',
- state: 'available',
- external_url: 'https://www.gitlab.com',
- environment_type: null,
- last_deployment: {
- id: 64,
- iid: 5,
- sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- ref: {
- name: 'master',
- ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
- },
- tag: false,
- '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: 1278,
- name: 'build',
- build_path: '/root/ci-folders/builds/1278',
- retry_path: '/root/ci-folders/builds/1278/retry',
- },
- manual_actions: [],
- },
- 'stoppable?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
- },
- {
- id: 32,
- name: 'review_app',
- state: 'stopped',
- external_url: 'https://www.gitlab.com',
- environment_type: null,
- last_deployment: {
- id: 64,
- iid: 5,
- sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- ref: {
- name: 'master',
- ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
- },
- tag: false,
- '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: 1278,
- name: 'build',
- build_path: '/root/ci-folders/builds/1278',
- retry_path: '/root/ci-folders/builds/1278/retry',
- },
- manual_actions: [],
- },
- 'stoppable?': false,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
- },
- {
- id: 33,
- name: 'test-environment',
- state: 'available',
- environment_type: 'review',
- last_deployment: null,
- 'stoppable?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
- },
- {
- id: 34,
- name: 'test-environment-1',
- state: 'available',
- environment_type: 'review',
- last_deployment: null,
- 'stoppable?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
- },
-];
-
-const environment = {
- id: 4,
- name: 'production',
- state: 'available',
- external_url: 'http://production.',
- environment_type: null,
- last_deployment: {},
- 'stoppable?': false,
- environment_path: '/root/review-app/environments/4',
- stop_path: '/root/review-app/environments/4/stop',
- created_at: '2016-12-16T11:51:04.690Z',
- updated_at: '2016-12-16T12:04:51.133Z',
-};
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 3949c5615d5..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(this);
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
deleted file mode 100644
index c5b86d35204..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 5cd0e5ab0f0..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(this);
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
deleted file mode 100644
index 3b71c255b30..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 5eba4343a1d..00000000000
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
+++ /dev/null
@@ -1,40 +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.FilteredSearchDropdown.prototype, 'constructor').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');
- });
- });
- });
-})();
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..5c65903701b
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -0,0 +1,283 @@
+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(`
+ <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/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
deleted file mode 100644
index 89e49b7c511..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 4bd45eb457d..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..ae9c263d1d7
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -0,0 +1,250 @@
+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();
+ });
+ });
+ });
+})();
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 a508dacf7f0..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ /dev/null
@@ -1,69 +0,0 @@
-/* global Turbolinks */
-
-//= require turbolinks
-//= 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(Turbolinks, 'visit').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- });
-
- manager.search();
- });
-
- it('should search with multiple words', () => {
- getInput().value = 'awesome search terms';
-
- spyOn(Turbolinks, 'visit').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
- });
-
- manager.search();
- });
-
- it('should search with special characters', () => {
- getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
-
- spyOn(Turbolinks, 'visit').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 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
new file mode 100644
index 00000000000..cf409a7e509
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -0,0 +1,110 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
+
+(() => {
+ describe('Filtered Search Token Keys', () => {
+ describe('get', () => {
+ let tokenKeys;
+
+ beforeEach(() => {
+ tokenKeys = gl.FilteredSearchTokenKeys.get();
+ });
+
+ it('should return tokenKeys', () => {
+ expect(tokenKeys !== null).toBe(true);
+ });
+
+ it('should return tokenKeys as an array', () => {
+ expect(tokenKeys instanceof Array).toBe(true);
+ });
+ });
+
+ describe('getConditions', () => {
+ let conditions;
+
+ beforeEach(() => {
+ conditions = gl.FilteredSearchTokenKeys.getConditions();
+ });
+
+ it('should return conditions', () => {
+ expect(conditions !== null).toBe(true);
+ });
+
+ it('should return conditions as an array', () => {
+ expect(conditions instanceof Array).toBe(true);
+ });
+ });
+
+ describe('searchByKey', () => {
+ it('should return null when key not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchBySymbol', () => {
+ it('should return null when symbol not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by symbol', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByKeyParam', () => {
+ it('should return null when key param not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+
+ it('should return alternative tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByConditionUrl', () => {
+ it('should return null when condition url not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by url', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
+ expect(result).toBe(conditions[0]);
+ });
+ });
+
+ describe('searchByConditionKeyValue', () => {
+ it('should return null when condition tokenKey and value not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by tokenKey and value', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
+ expect(result).toEqual(conditions[0]);
+ });
+ });
+ });
+})();
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.es6
deleted file mode 100644
index 9d9097419ea..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
+++ /dev/null
@@ -1,110 +0,0 @@
-//= require extensions/array
-//= require filtered_search/filtered_search_token_keys
-
-(() => {
- describe('Filtered Search Token Keys', () => {
- describe('get', () => {
- let tokenKeys;
-
- beforeEach(() => {
- tokenKeys = gl.FilteredSearchTokenKeys.get();
- });
-
- it('should return tokenKeys', () => {
- expect(tokenKeys !== null).toBe(true);
- });
-
- it('should return tokenKeys as an array', () => {
- expect(tokenKeys instanceof Array).toBe(true);
- });
- });
-
- describe('getConditions', () => {
- let conditions;
-
- beforeEach(() => {
- conditions = gl.FilteredSearchTokenKeys.getConditions();
- });
-
- it('should return conditions', () => {
- expect(conditions !== null).toBe(true);
- });
-
- it('should return conditions as an array', () => {
- expect(conditions instanceof Array).toBe(true);
- });
- });
-
- describe('searchByKey', () => {
- it('should return null when key not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by key', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchBySymbol', () => {
- it('should return null when symbol not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by symbol', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchByKeyParam', () => {
- it('should return null when key param not found', () => {
- const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
- expect(tokenKey === null).toBe(true);
- });
-
- it('should return tokenKey when found by key param', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.get();
- const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
- expect(result).toEqual(tokenKeys[0]);
- });
-
- it('should return alternative tokenKey when found by key param', () => {
- const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
- const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
- expect(result).toEqual(tokenKeys[0]);
- });
- });
-
- describe('searchByConditionUrl', () => {
- it('should return null when condition url not found', () => {
- const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
- expect(condition === null).toBe(true);
- });
-
- it('should return condition when found by url', () => {
- const conditions = gl.FilteredSearchTokenKeys.getConditions();
- const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
- expect(result).toBe(conditions[0]);
- });
- });
-
- describe('searchByConditionKeyValue', () => {
- it('should return null when condition tokenKey and value not found', () => {
- const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
- expect(condition === null).toBe(true);
- });
-
- it('should return condition when found by tokenKey and value', () => {
- const conditions = gl.FilteredSearchTokenKeys.getConditions();
- const result = gl.FilteredSearchTokenKeys
- .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
- expect(result).toEqual(conditions[0]);
- });
- });
- });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
new file mode 100644
index 00000000000..a91801cfc89
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
@@ -0,0 +1,127 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+
+(() => {
+ describe('Filtered Search Tokenizer', () => {
+ describe('processTokens', () => {
+ it('returns for input containing only search value', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(0);
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing only tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
+ expect(results.searchToken).toBe('');
+ expect(results.tokens.length).toBe(4);
+ expect(results.tokens[3]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Very Important"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('v1.0');
+ expect(results.tokens[2].symbol).toBe('%');
+
+ expect(results.tokens[3].key).toBe('assignee');
+ expect(results.tokens[3].value).toBe('none');
+ expect(results.tokens[3].symbol).toBe('');
+ });
+
+ it('returns for input starting with search value and ending with tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('searchTerm anotherSearchTerm milestone:none');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0]).toBe(results.lastToken);
+ expect(results.tokens[0].key).toBe('milestone');
+ expect(results.tokens[0].value).toBe('none');
+ expect(results.tokens[0].symbol).toBe('');
+ });
+
+ it('returns for input starting with tokens and ending with search value', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('assignee:@user searchTerm');
+
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('assignee');
+ expect(results.tokens[0].value).toBe('user');
+ expect(results.tokens[0].symbol).toBe('@');
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing search value wrapped between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
+
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Won\'t fix"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('none');
+ expect(results.tokens[2].symbol).toBe('');
+ });
+
+ it('returns for input containing search value in between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('assignee');
+ expect(results.tokens[1].value).toBe('none');
+ expect(results.tokens[1].symbol).toBe('');
+
+ expect(results.tokens[2].key).toBe('label');
+ expect(results.tokens[2].value).toBe('Doing');
+ expect(results.tokens[2].symbol).toBe('~');
+ });
+
+ it('returns search value for invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ expect(results.tokens.length).toEqual(0);
+ });
+
+ it('returns search value and token for mix of valid and invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
+ expect(results.tokens.length).toEqual(1);
+ expect(results.tokens[0].key).toBe('label');
+ expect(results.tokens[0].value).toBe('real');
+ expect(results.tokens[0].symbol).toBe('');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ });
+
+ it('returns search value for invalid symbols', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
+ expect(results.lastToken).toBe('std::includes');
+ expect(results.searchToken).toBe('std::includes');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
deleted file mode 100644
index ac7f8e9cbcd..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
+++ /dev/null
@@ -1,104 +0,0 @@
-//= require extensions/array
-//= require filtered_search/filtered_search_token_keys
-//= require filtered_search/filtered_search_tokenizer
-
-(() => {
- describe('Filtered Search Tokenizer', () => {
- describe('processTokens', () => {
- it('returns for input containing only search value', () => {
- const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
- expect(results.searchToken).toBe('searchTerm');
- expect(results.tokens.length).toBe(0);
- expect(results.lastToken).toBe(results.searchToken);
- });
-
- it('returns for input containing only tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
- expect(results.searchToken).toBe('');
- expect(results.tokens.length).toBe(4);
- expect(results.tokens[3]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('label');
- expect(results.tokens[1].value).toBe('"Very Important"');
- expect(results.tokens[1].symbol).toBe('~');
-
- expect(results.tokens[2].key).toBe('milestone');
- expect(results.tokens[2].value).toBe('v1.0');
- expect(results.tokens[2].symbol).toBe('%');
-
- expect(results.tokens[3].key).toBe('assignee');
- expect(results.tokens[3].value).toBe('none');
- expect(results.tokens[3].symbol).toBe('');
- });
-
- it('returns for input starting with search value and ending with tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('searchTerm anotherSearchTerm milestone:none');
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(1);
- expect(results.tokens[0]).toBe(results.lastToken);
- expect(results.tokens[0].key).toBe('milestone');
- expect(results.tokens[0].value).toBe('none');
- expect(results.tokens[0].symbol).toBe('');
- });
-
- it('returns for input starting with tokens and ending with search value', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('assignee:@user searchTerm');
-
- expect(results.searchToken).toBe('searchTerm');
- expect(results.tokens.length).toBe(1);
- expect(results.tokens[0].key).toBe('assignee');
- expect(results.tokens[0].value).toBe('user');
- expect(results.tokens[0].symbol).toBe('@');
- expect(results.lastToken).toBe(results.searchToken);
- });
-
- it('returns for input containing search value wrapped between tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
-
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(3);
- expect(results.tokens[2]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('label');
- expect(results.tokens[1].value).toBe('"Won\'t fix"');
- expect(results.tokens[1].symbol).toBe('~');
-
- expect(results.tokens[2].key).toBe('milestone');
- expect(results.tokens[2].value).toBe('none');
- expect(results.tokens[2].symbol).toBe('');
- });
-
- it('returns for input containing search value in between tokens', () => {
- const results = gl.FilteredSearchTokenizer
- .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
- expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
- expect(results.tokens.length).toBe(3);
- expect(results.tokens[2]).toBe(results.lastToken);
-
- expect(results.tokens[0].key).toBe('author');
- expect(results.tokens[0].value).toBe('root');
- expect(results.tokens[0].symbol).toBe('@');
-
- expect(results.tokens[1].key).toBe('assignee');
- expect(results.tokens[1].value).toBe('none');
- expect(results.tokens[1].symbol).toBe('');
-
- expect(results.tokens[2].key).toBe('label');
- expect(results.tokens[2].value).toBe('Doing');
- expect(results.tokens[2].symbol).toBe('~');
- });
- });
- });
-})();
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/.gitignore b/spec/javascripts/fixtures/.gitignore
index 009b68d5d1c..0c35cdd778e 100644
--- a/spec/javascripts/fixtures/.gitignore
+++ b/spec/javascripts/fixtures/.gitignore
@@ -1 +1,2 @@
*.html.raw
+*.json
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/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
deleted file mode 100644
index dc2ceed42f4..00000000000
--- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%form.js-quick-submit{ action: '/foo' }
- %input{ type: 'text', class: 'quick-submit-input'}
- %textarea
-
- %input{ type: 'submit'} Submit
- %button.btn{ type: 'submit' } Submit
diff --git a/spec/javascripts/fixtures/behaviors/requires_input.html.haml b/spec/javascripts/fixtures/behaviors/requires_input.html.haml
deleted file mode 100644
index c3f905e912e..00000000000
--- a/spec/javascripts/fixtures/behaviors/requires_input.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-%form.js-requires-input
- %input{type: 'text', id: 'required1', required: 'required'}
- %input{type: 'text', id: 'required2', required: 'required'}
- %input{type: 'text', id: 'required3', required: 'required', value: 'Pre-filled'}
- %input{type: 'text', id: 'optional1'}
-
- %textarea{id: 'required4', required: 'required'}
- %textarea{id: 'optional2'}
-
- %select{id: 'required5', required: 'required'}
- %option Zero
- %option{value: '1'} One
- %select{id: 'optional3', required: 'required'}
- %option Zero
- %option{value: '1'} One
-
- %button.submit{type: 'submit', value: 'Submit'}
- %input.submit{type: 'submit', value: 'Submit'}
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
new file mode 100644
index 00000000000..a059818145b
--- /dev/null
+++ b/spec/javascripts/fixtures/branches.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('branches/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'branches/new_branch.html.raw' do |example|
+ get :new,
+ namespace_id: project.namespace.to_param,
+ project_id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
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/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml
deleted file mode 100644
index 32446acfd60..00000000000
--- a/spec/javascripts/fixtures/dashboard.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-%ul.nav.nav-sidebar
- %li.home.active
- %a.dashboard-shortcuts-projects
- %span
- Projects
- %li
- %a
- %span
- Todos
- %span.count.js-todos-count
- 1
- %li
- %a.dashboard-shortcuts-activity
- %span
- Activity
- %li
- %a
- %span
- Groups
- %li
- %a
- %span
- Milestones
- %li
- %a.dashboard-shortcuts-issues
- %span
- Issues
- %span
- 1
- %li
- %a.dashboard-shortcuts-merge_requests
- %span
- Merge Requests
- %li
- %a
- %span
- Snippets
- %li
- %a
- %span
- Help
- %li
- %a
- %span
- Profile Settings
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
deleted file mode 100644
index 2ef242901e8..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(this);
diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
new file mode 100644
index 00000000000..aceec139730
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
@@ -0,0 +1,7 @@
+%div
+ #environments-folder-list-view{ data: { "can-create-deployment" => "true",
+ "can-read-environment" => "true",
+ "css-class" => "",
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "terminal-icon-svg" => custom_icon("icon_terminal"),
+ "play-icon-svg" => custom_icon("icon_play") } }
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/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
index 1ea1725c561..59edc0396d2 100644
--- a/spec/javascripts/fixtures/environments/table.html.haml
+++ b/spec/javascripts/fixtures/environments/table.html.haml
@@ -3,7 +3,7 @@
%tr
%th Environment
%th Last deployment
- %th Build
+ %th Job
%th Commit
%th
%th
diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml
deleted file mode 100644
index 4db2ef604de..00000000000
--- a/spec/javascripts/fixtures/header.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class
- .container-fluid
- .header-content
- %button.side-nav-toggle
- %span.sr-only
- Toggle navigation
- %i.fa.fa-bars
- %button.navbar-toggle
- %span.sr-only
- Toggle navigation
- %i.fa.fa-ellipsis-v
- .navbar-collapse.collapse
- %ui.nav.navbar-nav
- %li.hidden-sm.hidden-xs
- %li.visible-sm.visible-xs
- %li
- %a
- %i.fa.fa-bell.fa-fw
- %span.badge.todos-pending-count
- %li
- %a
- %i.fa.fa-plus.fa-fw
- %li.header-user.dropdown
- %a
- %img
- %span.caret
- .dropdown-menu-nav
- .dropdown-menu-align-right
- %ul
- %li
- %a.profile-link
- %li
- %a
- %li.divider
- %li.sign-out-link
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_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml
deleted file mode 100644
index 68678c3d7e3..00000000000
--- a/spec/javascripts/fixtures/merge_request_tabs.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-%ul.nav.nav-tabs.merge-request-tabs
- %li.notes-tab
- %a{href: '/foo/bar/merge_requests/1', data: {target: 'div#notes', action: 'notes', toggle: 'tab'}}
- Discussion
- %li.commits-tab
- %a{href: '/foo/bar/merge_requests/1/commits', data: {target: 'div#commits', action: 'commits', toggle: 'tab'}}
- Commits
- %li.diffs-tab
- %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'}}
- Diffs
-
-.tab-content
- #notes.notes.tab-pane
- Notes Content
- #commits.commits.tab-pane
- Commits Content
- #diffs.diffs.tab-pane
- Diffs Content
-
-.mr-loading-status
- .loading
- Loading Animation
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
new file mode 100644
index 00000000000..ee893b76c84
--- /dev/null
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('merge_requests/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
+ merge_request = create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item')
+ render_merge_request(example.description, merge_request)
+ end
+
+ private
+
+ def render_merge_request(fixture_file_name, merge_request)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, fixture_file_name)
+ end
+end
diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml
deleted file mode 100644
index f06629e5ecc..00000000000
--- a/spec/javascripts/fixtures/new_branch.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%form.js-create-branch-form
- %input.js-branch-name
- .js-branch-name-error
- %input{id: "ref"}
diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml
new file mode 100644
index 00000000000..fbe4a434f76
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines_table.html.haml
@@ -0,0 +1,2 @@
+#commit-pipeline-table-view{ data: { endpoint: "endpoint" } }
+.pipeline-svgs{ data: { "commit_icon_svg": "svg"} }
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/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml
deleted file mode 100644
index 9d1f7877116..00000000000
--- a/spec/javascripts/fixtures/project_title.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.header-content
- %h1.title
- %a
- GitLab Org
- %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"}
- GitLab Test
- %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" }
- .js-dropdown-menu-projects
- .dropdown-menu.dropdown-select.dropdown-menu-projects
- .dropdown-title
- %span Go to a project
- %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"}
- %i.fa.fa-times.dropdown-menu-close-icon
- .dropdown-input
- %input.dropdown-input-field{id: "", placeholder: "Search your projects", 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-loading
- %i.fa.fa-spinner.fa-spin
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
new file mode 100644
index 00000000000..6c33b240e5c
--- /dev/null
+++ b/spec/javascripts/fixtures/projects.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('projects/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'projects/dashboard.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
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.json b/spec/javascripts/fixtures/todos.json
deleted file mode 100644
index 62c2387d515..00000000000
--- a/spec/javascripts/fixtures/todos.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "count": 1,
- "delete_path": "/dashboard/todos/1"
-} \ No newline at end of file
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
new file mode 100644
index 00000000000..a81ef8c5492
--- /dev/null
+++ b/spec/javascripts/fixtures/todos.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'Todos (JavaScript fixtures)' do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) }
+
+ before(:all) do
+ clean_frontend_fixtures('todos/')
+ end
+
+ describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'todos/todos.html.raw' do |example|
+ get :index
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+
+ describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'todos/todos.json' do |example|
+ post :create,
+ namespace_id: namespace,
+ project_id: project,
+ issuable_type: 'issue',
+ issuable_id: issue_2.id,
+ format: 'json'
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+end
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 b/spec/javascripts/gfm_auto_complete_spec.js
new file mode 100644
index 00000000000..5dfa4008fbd
--- /dev/null
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -0,0 +1,148 @@
+/* eslint no-param-reassign: "off" */
+
+require('~/gfm_auto_complete');
+require('vendor/jquery.caret');
+require('vendor/jquery.atwho');
+
+const global = window.gl || (window.gl = {});
+const GfmAutoComplete = global.GfmAutoComplete;
+
+describe('GfmAutoComplete', function () {
+ describe('DefaultOptions.sorter', function () {
+ describe('assets loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true);
+
+ this.atwhoInstance = { setting: {} };
+ this.items = [];
+
+ this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
+ .call(this.atwhoInstance, '', this.items);
+ });
+
+ it('should disable highlightFirst', function () {
+ expect(this.atwhoInstance.setting.highlightFirst).toBe(false);
+ });
+
+ it('should return the passed unfiltered items', function () {
+ expect(this.sorterValue).toEqual(this.items);
+ });
+ });
+
+ describe('assets finished loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false);
+ spyOn($.fn.atwho.default.callbacks, 'sorter');
+ });
+
+ it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
+ const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should enable highlightFirst if a query is present', function () {
+ const atwhoInstance = { setting: {} };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should call the default atwho sorter', function () {
+ const atwhoInstance = { setting: {} };
+
+ const query = 'query';
+ const items = [];
+ const searchKey = 'searchKey';
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey);
+
+ expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
+ });
+ });
+ });
+
+ describe('DefaultOptions.matcher', function () {
+ const defaultMatcher = (context, flag, subtext) => (
+ GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext)
+ );
+
+ const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%'];
+ const otherFlags = ['/', ':'];
+ const flags = flagsUseDefaultMatcher.concat(otherFlags);
+
+ const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {});
+ const atwhoInstance = { setting: {}, app: { controllers: flagsHash } };
+
+ const minLen = 1;
+ const maxLen = 20;
+ const argumentSize = [minLen, maxLen / 2, maxLen];
+
+ const allowedSymbols = ['', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', '\'', '+', '-', '_'];
+ const jointAllowedSymbols = allowedSymbols.join('');
+
+ describe('should match regular symbols', () => {
+ flagsUseDefaultMatcher.forEach((flag) => {
+ allowedSymbols.forEach((symbol) => {
+ argumentSize.forEach((size) => {
+ const query = new Array(size + 1).join(symbol);
+ const subtext = flag + query;
+
+ it(`matches argument "${flag}" with query "${subtext}"`, () => {
+ expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query);
+ });
+ });
+ });
+
+ it(`matches combination of allowed symbols for flag "${flag}"`, () => {
+ const subtext = flag + jointAllowedSymbols;
+
+ expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols);
+ });
+ });
+ });
+
+ describe('should not match special sequences', () => {
+ const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+
+ flagsUseDefaultMatcher.forEach((atSign) => {
+ ShouldNotBeFollowedBy.forEach((followedSymbol) => {
+ const seq = atSign + followedSymbol;
+
+ it(`should not match "${seq}"`, () => {
+ expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
+ });
+ });
+ });
+ });
+ });
+
+ describe('isLoading', function () {
+ it('should be true with loading data object item', function () {
+ expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
+ });
+
+ it('should be true with loading data array', function () {
+ expect(GfmAutoComplete.isLoading(['loading'])).toBe(true);
+ });
+
+ it('should be true with loading data object array', function () {
+ expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true);
+ });
+
+ it('should be false with actual array data', function () {
+ expect(GfmAutoComplete.isLoading([
+ { title: 'Foo' },
+ { title: 'Bar' },
+ { title: 'Qux' },
+ ])).toBe(false);
+ });
+
+ it('should be false with actual data item', function () {
+ expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6
deleted file mode 100644
index 99cebb32a8b..00000000000
--- a/spec/javascripts/gfm_auto_complete_spec.js.es6
+++ /dev/null
@@ -1,91 +0,0 @@
-//= require gfm_auto_complete
-//= require jquery
-//= require jquery.atwho
-
-const global = window.gl || (window.gl = {});
-const GfmAutoComplete = global.GfmAutoComplete;
-
-describe('GfmAutoComplete', function () {
- describe('DefaultOptions.sorter', function () {
- describe('assets loading', function () {
- beforeEach(function () {
- spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true);
-
- this.atwhoInstance = { setting: {} };
- this.items = [];
-
- this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
- .call(this.atwhoInstance, '', this.items);
- });
-
- it('should disable highlightFirst', function () {
- expect(this.atwhoInstance.setting.highlightFirst).toBe(false);
- });
-
- it('should return the passed unfiltered items', function () {
- expect(this.sorterValue).toEqual(this.items);
- });
- });
-
- describe('assets finished loading', function () {
- beforeEach(function () {
- spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false);
- spyOn($.fn.atwho.default.callbacks, 'sorter');
- });
-
- it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
- const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
-
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
-
- expect(atwhoInstance.setting.highlightFirst).toBe(true);
- });
-
- it('should enable highlightFirst if a query is present', function () {
- const atwhoInstance = { setting: {} };
-
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
-
- expect(atwhoInstance.setting.highlightFirst).toBe(true);
- });
-
- it('should call the default atwho sorter', function () {
- const atwhoInstance = { setting: {} };
-
- const query = 'query';
- const items = [];
- const searchKey = 'searchKey';
-
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey);
-
- expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
- });
- });
- });
-
- describe('isLoading', function () {
- it('should be true with loading data object item', function () {
- expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
- });
-
- it('should be true with loading data array', function () {
- expect(GfmAutoComplete.isLoading(['loading'])).toBe(true);
- });
-
- it('should be true with loading data object array', function () {
- expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true);
- });
-
- it('should be false with actual array data', function () {
- expect(GfmAutoComplete.isLoading([
- { title: 'Foo' },
- { title: 'Bar' },
- { title: 'Qux' },
- ])).toBe(false);
- });
-
- it('should be false with actual data item', function () {
- expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
- });
- });
-});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
new file mode 100644
index 00000000000..c207fb00a47
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -0,0 +1,196 @@
+/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
+
+require('~/gl_dropdown');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('~/lib/utils/url_utility');
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let remoteCallback;
+
+ const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i += 1;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ const remoteMock = function remoteMock(data, term, callback) {
+ remoteCallback = callback.bind({}, data);
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
+
+ function initDropDown(hasRemote, isFilterable) {
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ }
+
+ beforeEach(() => {
+ loadFixtures('static/gl_dropdown.html.raw');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = getJSONFixture('projects.json');
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ initDropDown.call(this, false);
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ initDropDown.call(this, false, false);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(gl.utils, 'visitUrl').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ const linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should show loading indicator while search results are being fetched by backend', () => {
+ const dropdownMenu = document.querySelector('.dropdown-menu');
+
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
+ remoteCallback();
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
+ });
+
+ it('should not focus search input while remote task is not complete', () => {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', () => {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time', () => {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', () => {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ it('should still have input value on close and restore', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
+ });
+})();
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
deleted file mode 100644
index 06fa64b1b4e..00000000000
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ /dev/null
@@ -1,189 +0,0 @@
-/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
-/* global Turbolinks */
-
-/*= require jquery */
-/*= require gl_dropdown */
-/*= require turbolinks */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
-
-(() => {
- const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
- const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
- const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
- const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
-
- const ARROW_KEYS = {
- DOWN: 40,
- UP: 38,
- ENTER: 13,
- ESC: 27
- };
-
- let remoteCallback;
-
- const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
- i = i || 0;
- if (!i) direction = direction.toUpperCase();
- $('body').trigger({
- type: 'keydown',
- which: ARROW_KEYS[direction],
- keyCode: ARROW_KEYS[direction]
- });
- i += 1;
- if (i <= steps) {
- navigateWithKeys(direction, steps, cb, i);
- } else {
- cb();
- }
- };
-
- const remoteMock = function remoteMock(data, term, callback) {
- remoteCallback = callback.bind({}, data);
- };
-
- describe('Dropdown', function describeDropdown() {
- preloadFixtures('static/gl_dropdown.html.raw');
-
- function initDropDown(hasRemote, isFilterable) {
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
- selectable: true,
- filterable: isFilterable,
- data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
- search: {
- fields: ['name']
- },
- text: (project) => {
- (project.name_with_namespace || project.name);
- },
- id: (project) => {
- project.id;
- }
- });
- }
-
- beforeEach(() => {
- loadFixtures('static/gl_dropdown.html.raw');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
- });
-
- afterEach(() => {
- $('body').unbind('keydown');
- this.dropdownContainerElement.unbind('keyup');
- });
-
- it('should open on click', () => {
- initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
- });
-
- describe('that is open', () => {
- beforeEach(() => {
- initDropDown.call(this, false, false);
- this.dropdownButtonElement.click();
- });
-
- it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
- navigateWithKeys('down', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
- });
- });
-
- it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
- navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
- });
- });
- });
-
- it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
- navigateWithKeys('down', randomIndex, () => {
- spyOn(Turbolinks, 'visit').and.stub();
- navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
- expect(link).toHaveClass('is-active');
- const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
- });
- });
- });
-
- it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- });
- });
-
- describe('opened and waiting for a remote callback', () => {
- beforeEach(() => {
- initDropDown.call(this, true, true);
- this.dropdownButtonElement.click();
- });
-
- it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
-
- it('should focus search input after remote task is complete', () => {
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
-
- it('should focus on input when opening for the second time', () => {
- remoteCallback();
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- this.dropdownButtonElement.click();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
- });
-
- describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', () => {
- initDropDown.call(this, false, true);
- this.dropdownButtonElement.click();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
- });
-
- it('should still have input value on close and restore', () => {
- const $searchInput = $(SEARCH_INPUT_SELECTOR);
- initDropDown.call(this, false, true);
- $searchInput
- .trigger('focus')
- .val('g')
- .trigger('input');
- expect($searchInput.val()).toEqual('g');
- this.dropdownButtonElement.trigger('hidden.bs.dropdown');
- $searchInput
- .trigger('blur')
- .trigger('focus');
- expect($searchInput.val()).toEqual('g');
- });
- });
-})();
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
new file mode 100644
index 00000000000..9b44b25980c
--- /dev/null
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -0,0 +1,363 @@
+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',
+ },
+};
+
+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,
+ },
+ );
+ });
+ });
+
+ 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 b/spec/javascripts/gl_field_errors_spec.js
new file mode 100644
index 00000000000..733023481f5
--- /dev/null
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -0,0 +1,110 @@
+/* eslint-disable space-before-function-paren, arrow-body-style */
+
+require('~/gl_field_errors');
+
+((global) => {
+ preloadFixtures('static/gl_field_errors.html.raw');
+
+ describe('GL Style Field Errors', function() {
+ beforeEach(function() {
+ loadFixtures('static/gl_field_errors.html.raw');
+ const $form = this.$form = $('form.gl-show-field-errors');
+ this.fieldErrors = new global.GlFieldErrors($form);
+ });
+
+ it('should select the correct input elements', function() {
+ expect(this.$form).toBeDefined();
+ expect(this.$form.length).toBe(1);
+ expect(this.fieldErrors).toBeDefined();
+ const inputs = this.fieldErrors.state.inputs;
+ expect(inputs.length).toBe(4);
+ });
+
+ it('should ignore elements with custom error handling', function() {
+ const customErrorFlag = 'gl-field-error-ignore';
+ const customErrorElem = $(`.${customErrorFlag}`);
+
+ expect(customErrorElem.length).toBe(1);
+
+ const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+ return input.inputElement.hasClass(customErrorFlag);
+ });
+ expect(customErrors.length).toBe(0);
+ });
+
+ it('should not show any errors before submit attempt', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(0);
+ });
+
+ it('should show errors when input valid is submitted', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
+
+ this.$form.submit();
+
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(4);
+ });
+
+ it('should properly track validity state on input after invalid submission attempt', function() {
+ this.$form.submit();
+
+ const emailInputModel = this.fieldErrors.state.inputs[1];
+ const fieldState = emailInputModel.state;
+ const emailInputElement = emailInputModel.inputElement;
+
+ // No input
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then empty input
+ emailInputElement.val('').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+ });
+
+ it('should properly infer error messages', function() {
+ this.$form.submit();
+ const trackedInputs = this.fieldErrors.state.inputs;
+ const inputHasTitle = trackedInputs[1];
+ const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+ const inputNoTitle = trackedInputs[2];
+ const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+
+ expect(noTitleErrorElem.text()).toBe('This field is required.');
+ expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
deleted file mode 100644
index f68fd9e00d7..00000000000
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ /dev/null
@@ -1,111 +0,0 @@
-/* eslint-disable space-before-function-paren, arrow-body-style */
-
-//= require jquery
-//= require gl_field_errors
-
-((global) => {
- preloadFixtures('static/gl_field_errors.html.raw');
-
- describe('GL Style Field Errors', function() {
- beforeEach(function() {
- loadFixtures('static/gl_field_errors.html.raw');
- const $form = this.$form = $('form.gl-show-field-errors');
- this.fieldErrors = new global.GlFieldErrors($form);
- });
-
- it('should select the correct input elements', function() {
- expect(this.$form).toBeDefined();
- expect(this.$form.length).toBe(1);
- expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
- expect(inputs.length).toBe(4);
- });
-
- it('should ignore elements with custom error handling', function() {
- const customErrorFlag = 'gl-field-error-ignore';
- const customErrorElem = $(`.${customErrorFlag}`);
-
- expect(customErrorElem.length).toBe(1);
-
- const customErrors = this.fieldErrors.state.inputs.filter((input) => {
- return input.inputElement.hasClass(customErrorFlag);
- });
- expect(customErrors.length).toBe(0);
- });
-
- it('should not show any errors before submit attempt', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
-
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(0);
- });
-
- it('should show errors when input valid is submitted', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
-
- this.$form.submit();
-
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(4);
- });
-
- it('should properly track validity state on input after invalid submission attempt', function() {
- this.$form.submit();
-
- const emailInputModel = this.fieldErrors.state.inputs[1];
- const fieldState = emailInputModel.state;
- const emailInputElement = emailInputModel.inputElement;
-
- // No input
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then empty input
- emailInputElement.val('').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
- });
-
- it('should properly infer error messages', function() {
- this.$form.submit();
- const trackedInputs = this.fieldErrors.state.inputs;
- const inputHasTitle = trackedInputs[1];
- const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
- const inputNoTitle = trackedInputs[2];
- const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
-
- expect(noTitleErrorElem.text()).toBe('This field is required.');
- expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
new file mode 100644
index 00000000000..71d6e2a7e22
--- /dev/null
+++ b/spec/javascripts/gl_form_spec.js
@@ -0,0 +1,123 @@
+/* global autosize */
+
+window.autosize = require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
+require('~/lib/utils/common_utils');
+
+describe('GLForm', () => {
+ const global = window.gl || (window.gl = {});
+ const GLForm = global.GLForm;
+
+ it('should be defined in the global scope', () => {
+ expect(GLForm).toBeDefined();
+ });
+
+ describe('when instantiated', function () {
+ beforeEach((done) => {
+ this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ this.textarea = this.form.find('textarea');
+ spyOn($.prototype, 'off').and.returnValue(this.textarea);
+ spyOn($.prototype, 'on').and.returnValue(this.textarea);
+ spyOn($.prototype, 'css');
+ spyOn(window, 'autosize');
+
+ this.glForm = new GLForm(this.form);
+ setTimeout(() => {
+ $.prototype.off.calls.reset();
+ $.prototype.on.calls.reset();
+ $.prototype.css.calls.reset();
+ autosize.calls.reset();
+ done();
+ });
+ });
+
+ describe('.setupAutosize', () => {
+ beforeEach((done) => {
+ this.glForm.setupAutosize();
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('should register an autosize event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('autosize:resized');
+ expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function));
+ });
+
+ it('should register a mouseup event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize');
+ expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function));
+ });
+
+ it('should autosize the textarea', () => {
+ expect(autosize).toHaveBeenCalledWith(jasmine.any(Object));
+ });
+
+ it('should set the resize css property to vertical', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical');
+ });
+ });
+
+ describe('.setHeightData', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ this.glForm.setHeightData();
+ });
+
+ it('should set the height data attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+ });
+
+ describe('.destroyAutosize', () => {
+ describe('when called', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn(window, 'outerHeight').and.returnValue(400);
+ spyOn(autosize, 'destroy');
+
+ this.glForm.destroyAutosize();
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+
+ it('should get data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height');
+ });
+
+ it('should call autosize destroy', () => {
+ expect(autosize.destroy).toHaveBeenCalledWith(this.textarea);
+ });
+
+ it('should set the data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should set the outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalledWith(200);
+ });
+
+ it('should set the css', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('max-height', window.outerHeight);
+ });
+ });
+
+ it('should return undefined if the data-height equals the outerHeight', () => {
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn($.prototype, 'data').and.returnValue(200);
+ spyOn(autosize, 'destroy');
+ expect(this.glForm.destroyAutosize()).toBeUndefined();
+ expect(autosize.destroy).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6
deleted file mode 100644
index b5f99483bfb..00000000000
--- a/spec/javascripts/gl_form_spec.js.es6
+++ /dev/null
@@ -1,122 +0,0 @@
-/* global autosize */
-/*= require gl_form */
-/*= require autosize */
-/*= require lib/utils/text_utility */
-/*= require lib/utils/common_utils */
-
-describe('GLForm', () => {
- const global = window.gl || (window.gl = {});
- const GLForm = global.GLForm;
-
- it('should be defined in the global scope', () => {
- expect(GLForm).toBeDefined();
- });
-
- describe('when instantiated', function () {
- beforeEach((done) => {
- this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
- this.textarea = this.form.find('textarea');
- spyOn($.prototype, 'off').and.returnValue(this.textarea);
- spyOn($.prototype, 'on').and.returnValue(this.textarea);
- spyOn($.prototype, 'css');
- spyOn(window, 'autosize');
-
- this.glForm = new GLForm(this.form);
- setTimeout(() => {
- $.prototype.off.calls.reset();
- $.prototype.on.calls.reset();
- $.prototype.css.calls.reset();
- autosize.calls.reset();
- done();
- });
- });
-
- describe('.setupAutosize', () => {
- beforeEach((done) => {
- this.glForm.setupAutosize();
- setTimeout(() => {
- done();
- });
- });
-
- it('should register an autosize event handler on the textarea', () => {
- expect($.prototype.off).toHaveBeenCalledWith('autosize:resized');
- expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function));
- });
-
- it('should register a mouseup event handler on the textarea', () => {
- expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize');
- expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function));
- });
-
- it('should autosize the textarea', () => {
- expect(autosize).toHaveBeenCalledWith(jasmine.any(Object));
- });
-
- it('should set the resize css property to vertical', () => {
- expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical');
- });
- });
-
- describe('.setHeightData', () => {
- beforeEach(() => {
- spyOn($.prototype, 'data');
- spyOn($.prototype, 'outerHeight').and.returnValue(200);
- this.glForm.setHeightData();
- });
-
- it('should set the height data attribute', () => {
- expect($.prototype.data).toHaveBeenCalledWith('height', 200);
- });
-
- it('should call outerHeight', () => {
- expect($.prototype.outerHeight).toHaveBeenCalled();
- });
- });
-
- describe('.destroyAutosize', () => {
- describe('when called', () => {
- beforeEach(() => {
- spyOn($.prototype, 'data');
- spyOn($.prototype, 'outerHeight').and.returnValue(200);
- spyOn(window, 'outerHeight').and.returnValue(400);
- spyOn(autosize, 'destroy');
-
- this.glForm.destroyAutosize();
- });
-
- it('should call outerHeight', () => {
- expect($.prototype.outerHeight).toHaveBeenCalled();
- });
-
- it('should get data-height attribute', () => {
- expect($.prototype.data).toHaveBeenCalledWith('height');
- });
-
- it('should call autosize destroy', () => {
- expect(autosize.destroy).toHaveBeenCalledWith(this.textarea);
- });
-
- it('should set the data-height attribute', () => {
- expect($.prototype.data).toHaveBeenCalledWith('height', 200);
- });
-
- it('should set the outerHeight', () => {
- expect($.prototype.outerHeight).toHaveBeenCalledWith(200);
- });
-
- it('should set the css', () => {
- expect($.prototype.css).toHaveBeenCalledWith('max-height', window.outerHeight);
- });
- });
-
- it('should return undefined if the data-height equals the outerHeight', () => {
- spyOn($.prototype, 'outerHeight').and.returnValue(200);
- spyOn($.prototype, 'data').and.returnValue(200);
- spyOn(autosize, 'destroy');
- expect(this.glForm.destroyAutosize()).toBeUndefined();
- expect(autosize.destroy).not.toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index d76fcc5206a..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 63f28dfb8ad..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 71b589e6b83..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/header_spec.js b/spec/javascripts/header_spec.js
index b846c5ab00b..46a27b8c98f 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,12 +1,12 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require header */
-/*= require lib/utils/text_utility */
-/*= require jquery */
+
+require('~/header');
+require('~/lib/utils/text_utility');
(function() {
describe('Header', function() {
var todosPendingCount = '.todos-pending-count';
- var fixtureTemplate = 'static/header.html.raw';
+ var fixtureTemplate = 'issues/open-issue.html.raw';
function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden');
@@ -45,9 +45,9 @@
expect(isTodosCountHidden()).toEqual(false);
});
- it('should add delimiter to todos-pending-count', function() {
- expect($(todosPendingCount).text()).toEqual('1,000');
+ it('should show 99+ for todos-pending-count', function() {
+ expect($(todosPendingCount).text()).toEqual('99+');
});
});
});
-}).call(this);
+}).call(window);
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 92a20687ec5..00000000000
--- a/spec/javascripts/helpers/class_spec_helper.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-class ClassSpecHelper {
- static itShouldBeAStaticMethod(base, method) {
- return it('should be a static method', () => {
- expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
- });
- }
-}
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js
new file mode 100644
index 00000000000..0a61e561640
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
@@ -0,0 +1,36 @@
+/* global ClassSpecHelper */
+
+require('./class_spec_helper');
+
+describe('ClassSpecHelper', () => {
+ describe('.itShouldBeAStaticMethod', function () {
+ beforeEach(() => {
+ class TestClass {
+ instanceMethod() { this.prop = 'val'; }
+ static staticMethod() {}
+ }
+
+ this.TestClass = TestClass;
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
+
+ it('should have a defined spec', () => {
+ expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
+ });
+
+ it('should pass for a static method', () => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
+ expect(spec.status()).toBe('passed');
+ });
+
+ it('should fail for an instance method', (done) => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
+ spec.resultCallback = (result) => {
+ expect(result.status).toBe('failed');
+ done();
+ };
+ spec.execute();
+ });
+ });
+});
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
deleted file mode 100644
index d1155f1bd1e..00000000000
--- a/spec/javascripts/helpers/class_spec_helper_spec.js.es6
+++ /dev/null
@@ -1,35 +0,0 @@
-/* global ClassSpecHelper */
-//= require ./class_spec_helper
-
-describe('ClassSpecHelper', () => {
- describe('.itShouldBeAStaticMethod', function () {
- beforeEach(() => {
- class TestClass {
- instanceMethod() { this.prop = 'val'; }
- static staticMethod() {}
- }
-
- this.TestClass = TestClass;
- });
-
- ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
-
- it('should have a defined spec', () => {
- expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
- });
-
- it('should pass for a static method', () => {
- const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
- expect(spec.status()).toBe('passed');
- });
-
- it('should fail for an instance method', (done) => {
- const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
- spec.resultCallback = (result) => {
- expect(result.status).toBe('failed');
- done();
- };
- spec.execute();
- });
- });
-});
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 b/spec/javascripts/issuable_spec.js
new file mode 100644
index 00000000000..26d87cc5931
--- /dev/null
+++ b/spec/javascripts/issuable_spec.js
@@ -0,0 +1,80 @@
+/* global Issuable */
+
+require('~/lib/utils/url_utility');
+require('~/issuable');
+
+(() => {
+ const BASE_URL = '/user/project/issues?scope=all&state=closed';
+ const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
+
+ function updateForm(formValues, form) {
+ $.each(formValues, (id, value) => {
+ $(`#${id}`, form).val(value);
+ });
+ }
+
+ function resetForm(form) {
+ $('input[name!="utf8"]', form).each((index, input) => {
+ input.setAttribute('value', '');
+ });
+ }
+
+ describe('Issuable', () => {
+ preloadFixtures('static/issuable_filter.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/issuable_filter.html.raw');
+ Issuable.init();
+ });
+
+ it('should be defined', () => {
+ expect(window.Issuable).toBeDefined();
+ });
+
+ describe('filtering', () => {
+ let $filtersForm;
+
+ beforeEach(() => {
+ $filtersForm = $('.js-filter-form');
+ loadFixtures('static/issuable_filter.html.raw');
+ resetForm($filtersForm);
+ });
+
+ it('should contain only the default parameters', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ Issuable.filterResults($filtersForm);
+
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
+ });
+
+ it('should filter for the phrase "broken"', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ updateForm({ search: 'broken' }, $filtersForm);
+ Issuable.filterResults($filtersForm);
+ const params = `${DEFAULT_PARAMS}&search=broken`;
+
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ });
+
+ it('should keep query parameters after modifying filter', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ // initial filter
+ updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+
+ // update filter
+ updateForm({ label_name: 'Frontend' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6
deleted file mode 100644
index 917a6267b92..00000000000
--- a/spec/javascripts/issuable_spec.js.es6
+++ /dev/null
@@ -1,81 +0,0 @@
-/* global Issuable */
-/* global Turbolinks */
-
-//= require issuable
-//= require turbolinks
-
-(() => {
- const BASE_URL = '/user/project/issues?scope=all&state=closed';
- const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
-
- function updateForm(formValues, form) {
- $.each(formValues, (id, value) => {
- $(`#${id}`, form).val(value);
- });
- }
-
- function resetForm(form) {
- $('input[name!="utf8"]', form).each((index, input) => {
- input.setAttribute('value', '');
- });
- }
-
- describe('Issuable', () => {
- preloadFixtures('static/issuable_filter.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/issuable_filter.html.raw');
- Issuable.init();
- });
-
- it('should be defined', () => {
- expect(window.Issuable).toBeDefined();
- });
-
- describe('filtering', () => {
- let $filtersForm;
-
- beforeEach(() => {
- $filtersForm = $('.js-filter-form');
- loadFixtures('static/issuable_filter.html.raw');
- resetForm($filtersForm);
- });
-
- it('should contain only the default parameters', () => {
- spyOn(Turbolinks, 'visit');
-
- Issuable.filterResults($filtersForm);
-
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
- });
-
- it('should filter for the phrase "broken"', () => {
- spyOn(Turbolinks, 'visit');
-
- updateForm({ search: 'broken' }, $filtersForm);
- Issuable.filterResults($filtersForm);
- const params = `${DEFAULT_PARAMS}&search=broken`;
-
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
- });
-
- it('should keep query parameters after modifying filter', () => {
- spyOn(Turbolinks, 'visit');
-
- // initial filter
- updateForm({ milestone_title: 'v1.0' }, $filtersForm);
-
- Issuable.filterResults($filtersForm);
- let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
-
- // update filter
- updateForm({ label_name: 'Frontend' }, $filtersForm);
-
- Issuable.filterResults($filtersForm);
- params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
- });
- });
- });
-})();
diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js
new file mode 100644
index 00000000000..cb068a4f879
--- /dev/null
+++ b/spec/javascripts/issuable_time_tracker_spec.js
@@ -0,0 +1,202 @@
+/* eslint-disable */
+
+require('jquery');
+require('vue');
+require('~/issuable/time_tracking/components/time_tracker');
+
+function initTimeTrackingComponent(opts) {
+ setFixtures(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ this.initialData = {
+ time_estimate: opts.timeEstimate,
+ time_spent: opts.timeSpent,
+ human_time_estimate: opts.timeEstimateHumanReadable,
+ human_time_spent: opts.timeSpentHumanReadable,
+ docsUrl: '/help/workflow/time_tracking.md',
+ };
+
+ const TimeTrackingComponent = Vue.component('issuable-time-tracker');
+ this.timeTracker = new TimeTrackingComponent({
+ el: '#mock-container',
+ propsData: this.initialData,
+ });
+}
+
+((gl) => {
+ describe('Issuable Time Tracker', function() {
+ describe('Initialization', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should return something defined', function() {
+ expect(this.timeTracker).toBeDefined();
+ });
+
+ it ('should correctly set timeEstimate', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
+ done();
+ });
+ });
+ it ('should correctly set time_spent', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', function() {
+ describe('Panes', function() {
+ describe('Comparison pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+ Vue.nextTick(() => {
+ const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
+ expect(this.timeTracker.showComparisonState).toBe(true);
+ done();
+ });
+ });
+
+ describe('Remaining meter', function() {
+ it('should display the remaining meter with the correct width', function(done) {
+ Vue.nextTick(() => {
+ const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
+ const correctWidth = '5%';
+
+ expect(meterWidth).toBe(correctWidth);
+ done();
+ })
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', function(done) {
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done()
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', function(done) {
+ this.timeTracker.time_estimate = 100000;
+ this.timeTracker.time_spent = 20000000;
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done();
+ });
+ });
+ });
+ });
+
+ describe("Estimate only pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
+ });
+
+ it('should display the human readable version of time estimated', function(done) {
+ Vue.nextTick(() => {
+ const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should display the human readable version of time spent', function(done) {
+ Vue.nextTick(() => {
+ const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
+ Vue.nextTick(() => {
+ const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText =$noTrackingPane.innerText;
+ const correctText = 'No estimate or time spent';
+
+ expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe("Help pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
+ });
+
+ it('should not show the "Help" pane by default', function(done) {
+ Vue.nextTick(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+ done();
+ });
+ });
+
+ it('should show the "Help" pane when help button is clicked', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ expect(this.timeTracker.showHelpState).toBe(true);
+ expect($helpPane).toBeVisible();
+ done();
+ }, 10);
+ });
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+
+ $(this.timeTracker.$el).find('.close-help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+
+ done();
+ }, 1000);
+ }, 1000);
+ });
+ });
+ });
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6
deleted file mode 100644
index a1e979e8d09..00000000000
--- a/spec/javascripts/issuable_time_tracker_spec.js.es6
+++ /dev/null
@@ -1,201 +0,0 @@
-/* eslint-disable */
-//= require jquery
-//= require vue
-//= require issuable/time_tracking/components/time_tracker
-
-function initTimeTrackingComponent(opts) {
- fixture.set(`
- <div>
- <div id="mock-container"></div>
- </div>
- `);
-
- this.initialData = {
- time_estimate: opts.timeEstimate,
- time_spent: opts.timeSpent,
- human_time_estimate: opts.timeEstimateHumanReadable,
- human_time_spent: opts.timeSpentHumanReadable,
- docsUrl: '/help/workflow/time_tracking.md',
- };
-
- const TimeTrackingComponent = Vue.component('issuable-time-tracker');
- this.timeTracker = new TimeTrackingComponent({
- el: '#mock-container',
- propsData: this.initialData,
- });
-}
-
-((gl) => {
- describe('Issuable Time Tracker', function() {
- describe('Initialization', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
-
- it('should return something defined', function() {
- expect(this.timeTracker).toBeDefined();
- });
-
- it ('should correctly set timeEstimate', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
- done();
- });
- });
- it ('should correctly set time_spent', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
- done();
- });
- });
- });
-
- describe('Content Display', function() {
- describe('Panes', function() {
- describe('Comparison pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
- });
-
- it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
- Vue.nextTick(() => {
- const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
- expect(this.timeTracker.showComparisonState).toBe(true);
- done();
- });
- });
-
- describe('Remaining meter', function() {
- it('should display the remaining meter with the correct width', function(done) {
- Vue.nextTick(() => {
- const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
- const correctWidth = '5%';
-
- expect(meterWidth).toBe(correctWidth);
- done();
- })
- });
-
- it('should display the remaining meter with the correct background color when within estimate', function(done) {
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done()
- });
- });
-
- it('should display the remaining meter with the correct background color when over estimate', function(done) {
- this.timeTracker.time_estimate = 100000;
- this.timeTracker.time_spent = 20000000;
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done();
- });
- });
- });
- });
-
- describe("Estimate only pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
- });
-
- it('should display the human readable version of time estimated', function(done) {
- Vue.nextTick(() => {
- const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
- const correctText = 'Estimated: 2h 46m';
-
- expect(estimateText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('Spent only pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
-
- it('should display the human readable version of time spent', function(done) {
- Vue.nextTick(() => {
- const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
- const correctText = 'Spent: 1h 23m';
-
- expect(spentText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('No time tracking pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
- });
-
- it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
- Vue.nextTick(() => {
- const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
- const noTrackingText =$noTrackingPane.innerText;
- const correctText = 'No estimate or time spent';
-
- expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
- expect($noTrackingPane).toBeVisible();
- expect(noTrackingText).toBe(correctText);
- done();
- });
- });
- });
-
- describe("Help pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
- });
-
- it('should not show the "Help" pane by default', function(done) {
- Vue.nextTick(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
-
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
- done();
- });
- });
-
- it('should show the "Help" pane when help button is clicked', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
-
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
- expect(this.timeTracker.showHelpState).toBe(true);
- expect($helpPane).toBeVisible();
- done();
- }, 10);
- });
- });
-
- it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
-
- setTimeout(() => {
-
- $(this.timeTracker.$el).find('.close-help-button').click();
-
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
-
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
-
- done();
- }, 1000);
- }, 1000);
- });
- });
- });
- });
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 673a4b3c07a..8d25500b9fd 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 */
+require('~/lib/utils/text_utility');
-(function() {
+describe('Issue', function() {
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen;
@@ -59,28 +58,26 @@
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');
});
});
@@ -105,6 +102,7 @@
expectIssueState(false);
expect($btnClose).toHaveProp('disabled', false);
+ expect($('.issue_counter')).toHaveText(0);
});
it('fails to close an issue with success:false', function() {
@@ -121,6 +119,7 @@
expectIssueState(true);
expect($btnClose).toHaveProp('disabled', false);
expectErrorMessage();
+ expect($('.issue_counter')).toHaveText(1);
});
it('fails to closes an issue with HTTP error', function() {
@@ -135,6 +134,7 @@
expectIssueState(true);
expect($btnClose).toHaveProp('disabled', true);
expectErrorMessage();
+ expect($('.issue_counter')).toHaveText(1);
});
});
@@ -159,6 +159,7 @@
expectIssueState(true);
expect($btnReopen).toHaveProp('disabled', false);
+ expect($('.issue_counter')).toHaveText(1);
});
});
-}).call(this);
+});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
new file mode 100644
index 00000000000..37e038c16da
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -0,0 +1,90 @@
+/* eslint-disable no-new */
+/* global IssuableContext */
+/* global LabelsSelect */
+
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('select2');
+require('vendor/jquery.nicescroll');
+require('~/api');
+require('~/create_label');
+require('~/issuable_context');
+require('~/users_select');
+require('~/labels_select');
+
+(() => {
+ let saveLabelCount = 0;
+ describe('Issue dropdown sidebar', () => {
+ preloadFixtures('static/issue_sidebar_label.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/issue_sidebar_label.html.raw');
+ new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+ new LabelsSelect();
+
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const d = $.Deferred();
+ let LABELS_DATA = [];
+
+ if (req.url === '/root/test/labels.json') {
+ for (let i = 0; i < 10; i += 1) {
+ LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
+ }
+ } else if (req.url === '/root/test/issues/2.json') {
+ const tmp = [];
+ for (let i = 0; i < saveLabelCount; i += 1) {
+ tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
+ }
+ LABELS_DATA = { labels: tmp };
+ }
+
+ d.resolve(LABELS_DATA);
+ return d.promise();
+ });
+ });
+
+ it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+ saveLabelCount = 5;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+ done();
+ }, 0);
+ }, 0);
+ });
+
+ it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+ saveLabelCount = 6;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+})();
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
deleted file mode 100644
index 0d19b4a25b9..00000000000
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable no-new */
-/* global IssuableContext */
-/* global LabelsSelect */
-
-//= require lib/utils/type_utility
-//= require jquery
-//= require bootstrap
-//= require gl_dropdown
-//= require select2
-//= require jquery.nicescroll
-//= require api
-//= require create_label
-//= require issuable_context
-//= require users_select
-//= require labels_select
-
-(() => {
- let saveLabelCount = 0;
- describe('Issue dropdown sidebar', () => {
- preloadFixtures('static/issue_sidebar_label.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/issue_sidebar_label.html.raw');
- new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
- new LabelsSelect();
-
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- const d = $.Deferred();
- let LABELS_DATA = [];
-
- if (req.url === '/root/test/labels.json') {
- for (let i = 0; i < 10; i += 1) {
- LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- } else if (req.url === '/root/test/issues/2.json') {
- const tmp = [];
- for (let i = 0; i < saveLabelCount; i += 1) {
- tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- LABELS_DATA = { labels: tmp };
- }
-
- d.resolve(LABELS_DATA);
- return d.promise();
- });
- });
-
- it('changes collapsed tooltip when changing labels when less than 5', (done) => {
- saveLabelCount = 5;
- $('.edit-link').get(0).click();
-
- setTimeout(() => {
- expect($('.dropdown-content a').length).toBe(10);
-
- $('.dropdown-content a').each(function (i) {
- if (i < saveLabelCount) {
- $(this).get(0).click();
- }
- });
-
- $('.edit-link').get(0).click();
-
- setTimeout(() => {
- expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
- done();
- }, 0);
- }, 0);
- });
-
- it('changes collapsed tooltip when changing labels when more than 5', (done) => {
- saveLabelCount = 6;
- $('.edit-link').get(0).click();
-
- setTimeout(() => {
- expect($('.dropdown-content a').length).toBe(10);
-
- $('.dropdown-content a').each(function (i) {
- if (i < saveLabelCount) {
- $(this).get(0).click();
- }
- });
-
- $('.edit-link').get(0).click();
-
- setTimeout(() => {
- expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
- done();
- }, 0);
- }, 0);
- });
- });
-})();
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
new file mode 100644
index 00000000000..f4d3e77e515
--- /dev/null
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -0,0 +1,167 @@
+require('~/lib/utils/common_utils');
+
+(() => {
+ describe('common_utils', () => {
+ describe('gl.utils.parseUrl', () => {
+ it('returns an anchor tag with url', () => {
+ expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
+ });
+ it('url is escaped', () => {
+ // IE11 will return a relative pathname while other browsers will return a full pathname.
+ // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
+ // element will create an absolute url relative to the current execution context.
+ // The JavaScript test suite is executed at '/' which will lead to an absolute url
+ // starting with '/'.
+ expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
+ });
+ });
+
+ describe('gl.utils.parseUrlPathname', () => {
+ beforeEach(() => {
+ spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
+ pathname: url,
+ }));
+ });
+ it('returns an absolute url when given an absolute url', () => {
+ expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
+ });
+ it('returns an absolute url when given a relative url', () => {
+ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
+ });
+ });
+
+ describe('gl.utils.getUrlParamsArray', () => {
+ it('should return params array', () => {
+ expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true);
+ });
+
+ it('should remove the question mark from the search params', () => {
+ const paramsArray = gl.utils.getUrlParamsArray();
+ expect(paramsArray[0][0] !== '?').toBe(true);
+ });
+ });
+
+ describe('gl.utils.handleLocationHash', () => {
+ beforeEach(() => {
+ spyOn(window.document, 'getElementById').and.callThrough();
+ });
+
+ function expectGetElementIdToHaveBeenCalledWith(elementId) {
+ expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
+ }
+
+ it('decodes hash parameter', () => {
+ window.history.pushState({}, null, '#random-hash');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('random-hash');
+ expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
+ });
+
+ it('decodes cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#definição');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+
+ it('decodes encoded cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+ });
+
+ describe('gl.utils.getParameterByName', () => {
+ beforeEach(() => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+ });
+
+ it('should return valid parameter', () => {
+ const value = gl.utils.getParameterByName('scope');
+ expect(value).toBe('all');
+ });
+
+ it('should return invalid parameter', () => {
+ const value = gl.utils.getParameterByName('fakeParameter');
+ expect(value).toBe(null);
+ });
+ });
+
+ describe('gl.utils.normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
+
+ const normalized = gl.utils.normalizeHeaders(apiHeaders);
+
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
+ });
+ });
+
+ describe('gl.utils.parseIntPagination', () => {
+ it('should parse to integers all string values and return pagination object', () => {
+ const pagination = {
+ 'X-PER-PAGE': 10,
+ 'X-PAGE': 2,
+ 'X-TOTAL': 30,
+ 'X-TOTAL-PAGES': 3,
+ 'X-NEXT-PAGE': 3,
+ 'X-PREV-PAGE': 1,
+ };
+
+ const expectedPagination = {
+ perPage: 10,
+ page: 2,
+ total: 30,
+ totalPages: 3,
+ nextPage: 3,
+ previousPage: 1,
+ };
+
+ expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination);
+ });
+ });
+
+ describe('gl.utils.isMetaClick', () => {
+ it('should identify meta click on Windows/Linux', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify meta click on macOS', () => {
+ const e = {
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify as meta click on middle-click or Mouse-wheel click', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
deleted file mode 100644
index 1ce8f28e568..00000000000
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ /dev/null
@@ -1,73 +0,0 @@
-//= require lib/utils/common_utils
-
-(() => {
- describe('common_utils', () => {
- describe('gl.utils.parseUrl', () => {
- it('returns an anchor tag with url', () => {
- expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
- });
- it('url is escaped', () => {
- // IE11 will return a relative pathname while other browsers will return a full pathname.
- // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
- // element will create an absolute url relative to the current execution context.
- // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute
- // url starting with '/teaspoon'.
- expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22');
- });
- });
-
- describe('gl.utils.parseUrlPathname', () => {
- beforeEach(() => {
- spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
- pathname: url,
- }));
- });
- it('returns an absolute url when given an absolute url', () => {
- expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
- });
- it('returns an absolute url when given a relative url', () => {
- expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
- });
- });
-
- describe('gl.utils.getUrlParamsArray', () => {
- it('should return params array', () => {
- expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true);
- });
-
- it('should remove the question mark from the search params', () => {
- const paramsArray = gl.utils.getUrlParamsArray();
- expect(paramsArray[0][0] !== '?').toBe(true);
- });
- });
-
- describe('gl.utils.getParameterByName', () => {
- it('should return valid parameter', () => {
- const value = gl.utils.getParameterByName('reporter');
- expect(value).toBe('Console');
- });
-
- it('should return invalid parameter', () => {
- const value = gl.utils.getParameterByName('fakeParameter');
- expect(value).toBe(null);
- });
- });
-
- describe('gl.utils.normalizedHeaders', () => {
- it('should upperCase all the header keys to keep them consistent', () => {
- const apiHeaders = {
- 'X-Something-Workhorse': { workhorse: 'ok' },
- 'x-something-nginx': { nginx: 'ok' },
- };
-
- const normalized = gl.utils.normalizeHeaders(apiHeaders);
-
- const WORKHORSE = 'X-SOMETHING-WORKHORSE';
- const NGINX = 'X-SOMETHING-NGINX';
-
- expect(normalized[WORKHORSE].workhorse).toBe('ok');
- expect(normalized[NGINX].nginx).toBe('ok');
- });
- });
- });
-})();
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 e97356b65d5..00000000000
--- a/spec/javascripts/lib/utils/text_utility_spec.js.es6
+++ /dev/null
@@ -1,25 +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);
- });
- });
- });
-})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 6605986c33a..a1fd2d38968 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,22 +1,18 @@
/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */
/* global LineHighlighter */
-/*= require line_highlighter */
+require('~/line_highlighter');
(function() {
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 @@
});
});
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();
@@ -227,4 +217,4 @@
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index f644d39b1c7..fd97dced870 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,14 +1,14 @@
/* eslint-disable space-before-function-paren, no-return-assign */
/* global MergeRequest */
-/*= require merge_request */
+require('~/merge_request');
(function() {
describe('MergeRequest', function() {
return describe('task lists', function() {
- preloadFixtures('static/merge_requests_show.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() {
- loadFixtures('static/merge_requests_show.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
return this.merge = new MergeRequest();
});
it('modifies the Markdown field', function() {
@@ -19,11 +19,11 @@
return 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('/foo');
+ expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`);
return expect(req.data.merge_request.description).not.toBe(null);
});
return $('.js-task-list-field').trigger('tasklist:changed');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 98201fb98ed..7506e6ab49e 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,11 +1,20 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-/*= require merge_request_tabs */
-//= require breakpoints
-//= require lib/utils/common_utils
-//= require jquery.scrollTo
+require('~/merge_request_tabs');
+require('~/breakpoints');
+require('~/lib/utils/common_utils');
+require('vendor/jquery.scrollTo');
(function () {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ var phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('MergeRequestTabs', function () {
var stubLocation = {};
var setLocation = function (stubs) {
@@ -16,21 +25,23 @@
};
$.extend(stubLocation, defaults, stubs || {});
};
- preloadFixtures('static/merge_request_tabs.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
setLocation();
- this.spies = {
- history: spyOn(window.history, 'replaceState').and.callFake(function () {})
- };
+ if (!phantomjs) {
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function () {})
+ };
+ }
});
describe('#activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
- loadFixtures('static/merge_request_tabs.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.subject = this.class.activateTab;
});
it('shows the first tab when action is show', function () {
@@ -50,6 +61,84 @@
expect($('#diffs')).toHaveClass('active');
});
});
+ describe('#opensInNewTab', function () {
+ var tabUrl;
+ var windowTarget = '_blank';
+
+ beforeEach(function () {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+
+ tabUrl = $('.commits-tab a').attr('href');
+
+ spyOn($.fn, 'attr').and.returnValue(tabUrl);
+ });
+
+ describe('meta click', () => {
+ beforeEach(function () {
+ spyOn(gl.utils, 'isMetaClick').and.returnValue(true);
+ });
+
+ it('opens page when commits link is clicked', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
+ });
+
+ this.class.bindEvents();
+ document.querySelector('.merge-request-tabs .commits-tab a').click();
+ });
+
+ it('opens page when commits badge is clicked', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
+ });
+
+ this.class.bindEvents();
+ document.querySelector('.merge-request-tabs .commits-tab a .badge').click();
+ });
+ });
+
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
+ });
+
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual(windowTarget);
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ });
describe('#setCurrentAction', function () {
beforeEach(function () {
@@ -98,10 +187,11 @@
pathname: '/foo/bar/merge_requests/1'
});
newState = this.subject('commits');
- expect(this.spies.history).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState
- }, document.title, newState);
+ if (!phantomjs) {
+ expect(this.spies.history).toHaveBeenCalledWith({
+ url: newState
+ }, document.title, newState);
+ }
});
it('treats "show" like "notes"', function () {
setLocation({
@@ -119,4 +209,4 @@
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 6f1d6406897..d5193b41c33 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
-/*= require merge_request_widget */
-/*= require smart_interval */
-/*= require lib/utils/datetime_utility */
+require('~/merge_request_widget');
+require('~/smart_interval');
+require('~/lib/utils/datetime_utility');
(function() {
describe('MergeRequestWidget', function() {
@@ -189,4 +189,4 @@
});
});
});
-}).call(this);
+}).call(window);
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 a1c2fe3df37..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' });
-
- 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' });
-
- 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 8259d553f1b..90a429beeca 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,14 +1,13 @@
/* 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/autocomplete */
-/*= require new_branch_form */
+require('~/new_branch_form');
(function() {
describe('Branch', function() {
return describe('create a new branch', function() {
var expectToHaveError, fillNameWith;
- preloadFixtures('static/new_branch.html.raw');
+ preloadFixtures('branches/new_branch.html.raw');
fillNameWith = function(value) {
return $('.js-branch-name').val(value).trigger('blur');
};
@@ -16,7 +15,7 @@
return expect($('.js-branch-name-error span').text()).toEqual(error);
};
beforeEach(function() {
- loadFixtures('static/new_branch.html.raw');
+ loadFixtures('branches/new_branch.html.raw');
$('form').on('submit', function(e) {
return e.preventDefault();
});
@@ -166,4 +165,4 @@
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 015c35dfca7..d81a5bbb6a5 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,10 +1,10 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-/*= require notes */
-/*= require autosize */
-/*= require gl_form */
-/*= require lib/utils/text_utility */
+require('~/notes');
+require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
(function() {
window.gon || (window.gon = {});
@@ -35,15 +35,13 @@
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits the form on tasklist:changed', function() {
- var submitted = false;
- $('form').on('submit', function(e) {
- submitted = true;
- e.preventDefault();
+ 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('http://test.host/frontend-fixtures/issues-project/notes/1');
+ return expect(req.data.note).not.toBe(null);
});
-
$('.js-task-list-field').trigger('tasklist:changed');
- expect(submitted).toBe(true);
});
});
@@ -75,4 +73,4 @@
});
});
});
-}).call(this);
+}).call(window);
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 b/spec/javascripts/pipelines_spec.js
new file mode 100644
index 00000000000..72770a702d3
--- /dev/null
+++ b/spec/javascripts/pipelines_spec.js
@@ -0,0 +1,30 @@
+require('~/pipelines');
+
+// Fix for phantomJS
+if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
+ Element.prototype.matches = Element.prototype.webkitMatchesSelector;
+}
+
+(() => {
+ describe('Pipelines', () => {
+ preloadFixtures('static/pipeline_graph.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/pipeline_graph.html.raw');
+ });
+
+ it('should be defined', () => {
+ expect(window.gl.Pipelines).toBeDefined();
+ });
+
+ it('should create a `Pipelines` instance without options', () => {
+ expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
+ });
+
+ it('should create a `Pipelines` instance with options', () => {
+ const pipelines = new window.gl.Pipelines({ foo: 'bar' });
+
+ expect(pipelines.pipelineGraph).toBeDefined();
+ });
+ });
+})();
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
deleted file mode 100644
index f0f9ad7430d..00000000000
--- a/spec/javascripts/pipelines_spec.js.es6
+++ /dev/null
@@ -1,25 +0,0 @@
-//= require pipelines
-
-(() => {
- describe('Pipelines', () => {
- preloadFixtures('static/pipeline_graph.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/pipeline_graph.html.raw');
- });
-
- it('should be defined', () => {
- expect(window.gl.Pipelines).toBeDefined();
- });
-
- it('should create a `Pipelines` instance without options', () => {
- expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
- });
-
- it('should create a `Pipelines` instance with options', () => {
- const pipelines = new window.gl.Pipelines({ foo: 'bar' });
-
- expect(pipelines.pipelineGraph).toBeDefined();
- });
- });
-})();
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 b/spec/javascripts/pretty_time_spec.js
new file mode 100644
index 00000000000..a4662cfb557
--- /dev/null
+++ b/spec/javascripts/pretty_time_spec.js
@@ -0,0 +1,134 @@
+require('~/lib/utils/pretty_time');
+
+(() => {
+ const prettyTime = gl.utils.prettyTime;
+
+ describe('prettyTime methods', function () {
+ describe('parseSeconds', function () {
+ it('should correctly parse a negative value', function () {
+ const parser = prettyTime.parseSeconds;
+
+ const zeroSeconds = parser(-1000);
+
+ expect(zeroSeconds.minutes).toBe(16);
+ expect(zeroSeconds.hours).toBe(0);
+ expect(zeroSeconds.days).toBe(0);
+ expect(zeroSeconds.weeks).toBe(0);
+ });
+
+ it('should correctly parse a zero value', function () {
+ const parser = prettyTime.parseSeconds;
+
+ const zeroSeconds = parser(0);
+
+ expect(zeroSeconds.minutes).toBe(0);
+ expect(zeroSeconds.hours).toBe(0);
+ expect(zeroSeconds.days).toBe(0);
+ expect(zeroSeconds.weeks).toBe(0);
+ });
+
+ it('should correctly parse a small non-zero second values', function () {
+ const parser = prettyTime.parseSeconds;
+
+ const subOneMinute = parser(10);
+
+ expect(subOneMinute.minutes).toBe(0);
+ expect(subOneMinute.hours).toBe(0);
+ expect(subOneMinute.days).toBe(0);
+ expect(subOneMinute.weeks).toBe(0);
+
+ const aboveOneMinute = parser(100);
+
+ expect(aboveOneMinute.minutes).toBe(1);
+ expect(aboveOneMinute.hours).toBe(0);
+ expect(aboveOneMinute.days).toBe(0);
+ expect(aboveOneMinute.weeks).toBe(0);
+
+ const manyMinutes = parser(1000);
+
+ expect(manyMinutes.minutes).toBe(16);
+ expect(manyMinutes.hours).toBe(0);
+ expect(manyMinutes.days).toBe(0);
+ expect(manyMinutes.weeks).toBe(0);
+ });
+
+ it('should correctly parse large second values', function () {
+ const parser = prettyTime.parseSeconds;
+
+ const aboveOneHour = parser(4800);
+
+ expect(aboveOneHour.minutes).toBe(20);
+ expect(aboveOneHour.hours).toBe(1);
+ expect(aboveOneHour.days).toBe(0);
+ expect(aboveOneHour.weeks).toBe(0);
+
+ const aboveOneDay = parser(110000);
+
+ expect(aboveOneDay.minutes).toBe(33);
+ expect(aboveOneDay.hours).toBe(6);
+ expect(aboveOneDay.days).toBe(3);
+ expect(aboveOneDay.weeks).toBe(0);
+
+ const aboveOneWeek = parser(25000000);
+
+ expect(aboveOneWeek.minutes).toBe(26);
+ expect(aboveOneWeek.hours).toBe(0);
+ expect(aboveOneWeek.days).toBe(3);
+ expect(aboveOneWeek.weeks).toBe(173);
+ });
+ });
+
+ describe('stringifyTime', function () {
+ it('should stringify values with all non-zero units', function () {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
+
+ const timeString = prettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
+
+ it('should stringify values with some non-zero units', function () {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
+
+ const timeString = prettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('4d 20m');
+ });
+
+ it('should stringify values with no non-zero units', function () {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
+
+ const timeString = prettyTime.stringifyTime(timeObject);
+
+ expect(timeString).toBe('0m');
+ });
+ });
+
+ describe('abbreviateTime', function () {
+ it('should abbreviate stringified times for weeks', function () {
+ const fullTimeString = '1w 3d 4h 5m';
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
+ });
+
+ it('should abbreviate stringified times for non-weeks', function () {
+ const fullTimeString = '0w 3d 4h 5m';
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6
deleted file mode 100644
index 7a04fba5f7f..00000000000
--- a/spec/javascripts/pretty_time_spec.js.es6
+++ /dev/null
@@ -1,134 +0,0 @@
-//= require lib/utils/pretty_time
-
-(() => {
- const prettyTime = gl.utils.prettyTime;
-
- describe('prettyTime methods', function () {
- describe('parseSeconds', function () {
- it('should correctly parse a negative value', function () {
- const parser = prettyTime.parseSeconds;
-
- const zeroSeconds = parser(-1000);
-
- expect(zeroSeconds.minutes).toBe(16);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a zero value', function () {
- const parser = prettyTime.parseSeconds;
-
- const zeroSeconds = parser(0);
-
- expect(zeroSeconds.minutes).toBe(0);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a small non-zero second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const subOneMinute = parser(10);
-
- expect(subOneMinute.minutes).toBe(0);
- expect(subOneMinute.hours).toBe(0);
- expect(subOneMinute.days).toBe(0);
- expect(subOneMinute.weeks).toBe(0);
-
- const aboveOneMinute = parser(100);
-
- expect(aboveOneMinute.minutes).toBe(1);
- expect(aboveOneMinute.hours).toBe(0);
- expect(aboveOneMinute.days).toBe(0);
- expect(aboveOneMinute.weeks).toBe(0);
-
- const manyMinutes = parser(1000);
-
- expect(manyMinutes.minutes).toBe(16);
- expect(manyMinutes.hours).toBe(0);
- expect(manyMinutes.days).toBe(0);
- expect(manyMinutes.weeks).toBe(0);
- });
-
- it('should correctly parse large second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const aboveOneHour = parser(4800);
-
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
-
- const aboveOneDay = parser(110000);
-
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(3);
- expect(aboveOneDay.weeks).toBe(0);
-
- const aboveOneWeek = parser(25000000);
-
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(3);
- expect(aboveOneWeek.weeks).toBe(173);
- });
- });
-
- describe('stringifyTime', function () {
- it('should stringify values with all non-zero units', function () {
- const timeObject = {
- weeks: 1,
- days: 4,
- hours: 7,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('1w 4d 7h 20m');
- });
-
- it('should stringify values with some non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 4,
- hours: 0,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('4d 20m');
- });
-
- it('should stringify values with no non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 0,
- hours: 0,
- minutes: 0,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('0m');
- });
- });
-
- describe('abbreviateTime', function () {
- it('should abbreviate stringified times for weeks', function () {
- const fullTimeString = '1w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', function () {
- const fullTimeString = '0w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
- });
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 0202c9ba85e..69d9587771f 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,27 +1,28 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */
-
/* global Project */
-/*= require bootstrap */
-/*= require select2 */
-/*= require lib/utils/type_utility */
-/*= require gl_dropdown */
-/*= require api */
-/*= require project_select */
-/*= require project */
+require('select2/select2.js');
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('~/api');
+require('~/project_select');
+require('~/project');
(function() {
- window.gon || (window.gon = {});
-
- window.gon.api_version = 'v3';
-
describe('Project Title', function() {
- preloadFixtures('static/project_title.html.raw');
+ preloadFixtures('issues/open-issue.html.raw');
+ loadJSONFixtures('projects.json');
+
beforeEach(function() {
- loadFixtures('static/project_title.html.raw');
+ loadFixtures('issues/open-issue.html.raw');
+
+ window.gon = {};
+ window.gon.api_version = 'v3';
+
return this.project = new Project();
});
- return describe('project list', function() {
+
+ describe('project list', function() {
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
@@ -37,16 +38,17 @@
return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
- it('to show on toggle click', (function(_this) {
- return function() {
- $('.js-projects-dropdown-toggle').click();
- return expect($('.header-content').hasClass('open')).toBe(true);
- };
- })(this));
- return it('hide dropdown', function() {
- $(".dropdown-menu-close-icon").click();
- return expect($('.header-content').hasClass('open')).toBe(false);
+ it('toggles dropdown', function() {
+ var menu = $('.js-dropdown-menu-projects');
+ $('.js-projects-dropdown-toggle').click();
+ expect(menu).toHaveClass('open');
+ menu.find('.dropdown-menu-close-icon').click();
+ expect(menu).not.toHaveClass('open');
});
});
+
+ afterEach(() => {
+ window.gon = {};
+ });
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 942778229b5..285b7940174 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,11 +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 jquery */
-/*= require js.cookie */
-
-/*= require extensions/jquery.js */
+import '~/commons/bootstrap';
+import '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -37,6 +34,8 @@
describe('RightSidebar', function() {
var fixtureName = 'issues/open-issue.html.raw';
preloadFixtures(fixtureName);
+ loadJSONFixtures('todos/todos.json');
+
beforeEach(function() {
loadFixtures(fixtureName);
this.sidebar = new Sidebar;
@@ -65,7 +64,7 @@
});
it('should broadcast todo:toggle event when add todo clicked', function() {
- var todos = getJSONFixture('todos.json');
+ var todos = getJSONFixture('todos/todos.json');
spyOn(jQuery, 'ajax').and.callFake(function() {
var d = $.Deferred();
var response = todos;
@@ -80,4 +79,4 @@
expect(todoToggleSpy.calls.count()).toEqual(1);
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 7ac9710654f..aaf058bd755 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,13 +1,10 @@
/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
-/*= require gl_dropdown */
-/*= require search_autocomplete */
-/*= require jquery */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
-/*= require fuzzaldrin-plus */
-/*= require turbolinks */
-/*= require jquery.turbolinks */
+require('~/gl_dropdown');
+require('~/search_autocomplete');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('vendor/fuzzaldrin-plus');
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -17,11 +14,6 @@
userId = 1;
- window.gon || (window.gon = {});
-
- window.gon.current_user_id = userId;
- window.gon.current_username = userName;
-
dashboardIssuesPath = '/dashboard/issues';
dashboardMRsPath = '/dashboard/merge_requests';
@@ -97,8 +89,8 @@
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
- mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
- mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
+ mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
+ mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
a1 = "a[href='" + issuesAssignedToMeLink + "']";
a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']";
@@ -117,13 +109,25 @@
preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
+ widget = new gl.SearchAutocomplete;
+ // Prevent turbolinks from triggering within gl_dropdown
+ spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
+
+ window.gon = {};
+ window.gon.current_user_id = userId;
+ window.gon.current_username = userName;
+
return widget = new gl.SearchAutocomplete;
});
+
+ afterEach(function() {
+ window.gon = {};
+ });
it('should show Dashboard specific dropdown menu', function() {
var list;
addBodyAttributes();
mockDashboardOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
@@ -131,7 +135,7 @@
var list;
addBodyAttributes('group');
mockGroupOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
@@ -139,7 +143,7 @@
var list;
addBodyAttributes('project');
mockProjectOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
@@ -148,7 +152,7 @@
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
@@ -159,7 +163,7 @@
addBodyAttributes();
mockDashboardOptions(true);
var submitSpy = spyOnEvent('form', 'submit');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
var enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
@@ -171,4 +175,4 @@
expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index db11c2516a6..9e19dabd0e3 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
-/*= require copy_as_gfm */
-/*= require shortcuts_issuable */
+require('~/copy_as_gfm');
+require('~/shortcuts_issuable');
(function() {
describe('ShortcutsIssuable', function() {
@@ -31,13 +31,9 @@
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() {
@@ -59,12 +55,8 @@
expect(triggered).toBe(true);
});
it('triggers `focus`', function() {
- var focused = false;
- $(this.selector).on('focus', function() {
- focused = true;
- });
this.shortcut.replyWithSelectedText();
- expect(focused).toBe(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
});
describe('with a one-line selection', function() {
@@ -83,4 +75,4 @@
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
new file mode 100644
index 00000000000..d83d9a57b42
--- /dev/null
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -0,0 +1,53 @@
+require('~/signin_tabs_memoizer');
+
+((global) => {
+ describe('SigninTabsMemoizer', () => {
+ const fixtureTemplate = 'static/signin_tabs.html.raw';
+ const tabSelector = 'ul.nav-tabs';
+ const currentTabKey = 'current_signin_tab';
+ let memo;
+
+ function createMemoizer() {
+ memo = new global.ActiveTabMemoizer({
+ currentTabKey,
+ tabSelector,
+ });
+ return memo;
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ });
+
+ it('does nothing if no tab was previously selected', () => {
+ createMemoizer();
+
+ expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard');
+ });
+
+ it('shows last selected tab on boot', () => {
+ createMemoizer().saveData('#ldap');
+ const fakeTab = {
+ click: () => {},
+ };
+ spyOn(document, 'querySelector').and.returnValue(fakeTab);
+ spyOn(fakeTab, 'click');
+
+ memo.bootstrap();
+
+ // verify that triggers click on the last selected tab
+ expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`);
+ expect(fakeTab.click).toHaveBeenCalled();
+ });
+
+ it('saves last selected tab on change', () => {
+ createMemoizer();
+
+ document.getElementById('standard').click();
+
+ expect(memo.readData()).toEqual('#standard');
+ });
+ });
+})(window);
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
deleted file mode 100644
index c274b9c45f4..00000000000
--- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/*= require signin_tabs_memoizer */
-
-((global) => {
- describe('SigninTabsMemoizer', () => {
- const fixtureTemplate = 'static/signin_tabs.html.raw';
- const tabSelector = 'ul.nav-tabs';
- const currentTabKey = 'current_signin_tab';
- let memo;
-
- function createMemoizer() {
- memo = new global.ActiveTabMemoizer({
- currentTabKey,
- tabSelector,
- });
- return memo;
- }
-
- preloadFixtures(fixtureTemplate);
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- });
-
- it('does nothing if no tab was previously selected', () => {
- createMemoizer();
-
- expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard');
- });
-
- it('shows last selected tab on boot', () => {
- createMemoizer().saveData('#ldap');
- const fakeTab = {
- click: () => {},
- };
- spyOn(document, 'querySelector').and.returnValue(fakeTab);
- spyOn(fakeTab, 'click');
-
- memo.bootstrap();
-
- // verify that triggers click on the last selected tab
- expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`);
- expect(fakeTab.click).toHaveBeenCalled();
- });
-
- it('saves last selected tab on change', () => {
- createMemoizer();
-
- document.getElementById('standard').click();
-
- expect(memo.readData()).toEqual('#standard');
- });
- });
-})(window);
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
new file mode 100644
index 00000000000..4366ec2a5b8
--- /dev/null
+++ b/spec/javascripts/smart_interval_spec.js
@@ -0,0 +1,179 @@
+require('~/smart_interval');
+
+(() => {
+ const DEFAULT_MAX_INTERVAL = 100;
+ const DEFAULT_STARTING_INTERVAL = 5;
+ const DEFAULT_SHORT_TIMEOUT = 75;
+ const DEFAULT_LONG_TIMEOUT = 1000;
+ const DEFAULT_INCREMENT_FACTOR = 2;
+
+ function createDefaultSmartInterval(config) {
+ const defaultParams = {
+ callback: () => {},
+ startingInterval: DEFAULT_STARTING_INTERVAL,
+ maxInterval: DEFAULT_MAX_INTERVAL,
+ incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
+ lazyStart: false,
+ immediateExecution: false,
+ hiddenInterval: null,
+ };
+
+ if (config) {
+ _.extend(defaultParams, config);
+ }
+
+ return new gl.SmartInterval(defaultParams);
+ }
+
+ describe('SmartInterval', function () {
+ describe('Increment Interval', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should increment the interval delay', function (done) {
+ const interval = this.smartInterval;
+ setTimeout(() => {
+ const intervalConfig = this.smartInterval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations = intervalConfig.startingInterval *
+ (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
+ });
+
+ it('should not increment past maxInterval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ const currentInterval = interval.getCurrentInterval();
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
+
+ done();
+ }, DEFAULT_LONG_TIMEOUT);
+ });
+ });
+
+ describe('Public methods', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should cancel an interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ interval.cancel();
+
+ const intervalId = interval.state.intervalId;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
+
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should resume an interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ interval.cancel();
+
+ interval.resume();
+
+ const intervalId = interval.state.intervalId;
+
+ expect(intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+ });
+
+ describe('DOM Events', function () {
+ beforeEach(function () {
+ // This ensures DOM and DOM events are initialized for these specs.
+ setFixtures('<div></div>');
+
+ this.smartInterval = createDefaultSmartInterval();
+ });
+
+ it('should pause when page is not visible', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should change to the hidden interval when page is not visible', function (done) {
+ const HIDDEN_INTERVAL = 1500;
+ const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should resume when page is becomes visible at the previous interval', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should cancel on page unload', function (done) {
+ const interval = this.smartInterval;
+
+ setTimeout(() => {
+ $(document).triggerHandler('beforeunload');
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
+ it('should execute callback before first interval', function () {
+ const interval = createDefaultSmartInterval({ immediateExecution: true });
+ expect(interval.cfg.immediateExecution).toBeFalsy();
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6
deleted file mode 100644
index 39d236986b9..00000000000
--- a/spec/javascripts/smart_interval_spec.js.es6
+++ /dev/null
@@ -1,180 +0,0 @@
-//= require jquery
-//= require smart_interval
-
-(() => {
- const DEFAULT_MAX_INTERVAL = 100;
- const DEFAULT_STARTING_INTERVAL = 5;
- const DEFAULT_SHORT_TIMEOUT = 75;
- const DEFAULT_LONG_TIMEOUT = 1000;
- const DEFAULT_INCREMENT_FACTOR = 2;
-
- function createDefaultSmartInterval(config) {
- const defaultParams = {
- callback: () => {},
- startingInterval: DEFAULT_STARTING_INTERVAL,
- maxInterval: DEFAULT_MAX_INTERVAL,
- incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
- lazyStart: false,
- immediateExecution: false,
- hiddenInterval: null,
- };
-
- if (config) {
- _.extend(defaultParams, config);
- }
-
- return new gl.SmartInterval(defaultParams);
- }
-
- describe('SmartInterval', function () {
- describe('Increment Interval', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should increment the interval delay', function (done) {
- const interval = this.smartInterval;
- setTimeout(() => {
- const intervalConfig = this.smartInterval.cfg;
- const iterationCount = 4;
- const maxIntervalAfterIterations = intervalConfig.startingInterval *
- (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
- const currentInterval = interval.getCurrentInterval();
-
- // Provide some flexibility for performance of testing environment
- expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
- expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
-
- done();
- }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
- });
-
- it('should not increment past maxInterval', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- const currentInterval = interval.getCurrentInterval();
- expect(currentInterval).toBe(interval.cfg.maxInterval);
-
- done();
- }, DEFAULT_LONG_TIMEOUT);
- });
- });
-
- describe('Public methods', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should cancel an interval', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- interval.cancel();
-
- const intervalId = interval.state.intervalId;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
-
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
-
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
-
- it('should resume an interval', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- interval.cancel();
-
- interval.resume();
-
- const intervalId = interval.state.intervalId;
-
- expect(intervalId).toBeTruthy();
-
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
- });
-
- describe('DOM Events', function () {
- beforeEach(function () {
- // This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
-
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should pause when page is not visible', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
-
- it('should change to the hidden interval when page is not visible', function (done) {
- const HIDDEN_INTERVAL = 1500;
- const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
-
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
- interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
-
- it('should resume when page is becomes visible at the previous interval', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
-
- expect(interval.state.intervalId).toBeTruthy();
-
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
-
- it('should cancel on page unload', function (done) {
- const interval = this.smartInterval;
-
- setTimeout(() => {
- $(document).trigger('page:before-unload');
- expect(interval.state.intervalId).toBeUndefined();
- expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
-
- it('should execute callback before first interval', function () {
- const interval = createDefaultSmartInterval({ immediateExecution: true });
- expect(interval.cfg.immediateExecution).toBeFalsy();
- });
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
deleted file mode 100644
index f8e3aca29fa..00000000000
--- a/spec/javascripts/spec_helper.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable space-before-function-paren */
-// PhantomJS (Teaspoons default driver) doesn't have support for
-// Function.prototype.bind, which has caused confusion. Use this polyfill to
-// avoid the confusion.
-/*= require support/bind-poly */
-
-// You can require your own javascript files here. By default this will include
-// everything in application, however you may get better load performance if you
-// require the specific files that are being used in the spec that tests them.
-/*= require jquery */
-/*= require jquery.turbolinks */
-/*= require bootstrap */
-/*= require underscore */
-
-// Teaspoon includes some support files, but you can use anything from your own
-// support path too.
-// require support/jasmine-jquery-1.7.0
-// require support/jasmine-jquery-2.0.0
-/*= require support/jasmine-jquery-2.1.0 */
-
-// require support/sinon
-// require support/your-support-file
-// Deferring execution
-// If you're using CommonJS, RequireJS or some other asynchronous library you can
-// defer execution. Call Teaspoon.execute() after everything has been loaded.
-// Simple example of a timeout:
-// Teaspoon.defer = true
-// setTimeout(Teaspoon.execute, 1000)
-// Matching files
-// By default Teaspoon will look for files that match
-// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path
-// and it'll be included in the default suite automatically. If you want to
-// customize suites, check out the configuration in teaspoon_env.rb
-// Manifest
-// If you'd rather require your spec files manually (to control order for
-// instance) you can disable the suite matcher in the configuration and use this
-// file as a manifest.
-// For more information: http://github.com/modeset/teaspoon
-
-// set our fixtures path
-jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures';
-jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures';
-
-// defined in ActionDispatch::TestRequest
-// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7
-window.gl = window.gl || {};
-window.gl.TEST_HOST = 'http://test.host';
-window.gon = window.gon || {};
diff --git a/spec/javascripts/subbable_resource_spec.js b/spec/javascripts/subbable_resource_spec.js
new file mode 100644
index 00000000000..454386697f5
--- /dev/null
+++ b/spec/javascripts/subbable_resource_spec.js
@@ -0,0 +1,63 @@
+/* eslint-disable max-len, arrow-parens, comma-dangle */
+
+require('~/subbable_resource');
+
+/*
+* Test that each rest verb calls the publish and subscribe function and passes the correct value back
+*
+*
+* */
+((global) => {
+ describe('Subbable Resource', function () {
+ describe('PubSub', function () {
+ beforeEach(function () {
+ this.MockResource = new global.SubbableResource('https://example.com');
+ });
+ it('should successfully add a single subscriber', function () {
+ const callback = () => {};
+ this.MockResource.subscribe(callback);
+
+ expect(this.MockResource.subscribers.length).toBe(1);
+ expect(this.MockResource.subscribers[0]).toBe(callback);
+ });
+
+ it('should successfully add multiple subscribers', function () {
+ const callbackOne = () => {};
+ const callbackTwo = () => {};
+ const callbackThree = () => {};
+
+ this.MockResource.subscribe(callbackOne);
+ this.MockResource.subscribe(callbackTwo);
+ this.MockResource.subscribe(callbackThree);
+
+ expect(this.MockResource.subscribers.length).toBe(3);
+ });
+
+ it('should successfully publish an update to a single subscriber', function () {
+ const state = { myprop: 1 };
+
+ const callbacks = {
+ one: (data) => expect(data.myprop).toBe(2),
+ two: (data) => expect(data.myprop).toBe(2),
+ three: (data) => expect(data.myprop).toBe(2)
+ };
+
+ const spyOne = spyOn(callbacks, 'one');
+ const spyTwo = spyOn(callbacks, 'two');
+ const spyThree = spyOn(callbacks, 'three');
+
+ this.MockResource.subscribe(callbacks.one);
+ this.MockResource.subscribe(callbacks.two);
+ this.MockResource.subscribe(callbacks.three);
+
+ state.myprop += 1;
+
+ this.MockResource.publish(state);
+
+ expect(spyOne).toHaveBeenCalled();
+ expect(spyTwo).toHaveBeenCalled();
+ expect(spyThree).toHaveBeenCalled();
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
deleted file mode 100644
index 99f45850ea3..00000000000
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ /dev/null
@@ -1,66 +0,0 @@
-/* eslint-disable max-len, arrow-parens, comma-dangle */
-
-//= vue
-//= vue-resource
-//= require jquery
-//= require subbable_resource
-
-/*
-* Test that each rest verb calls the publish and subscribe function and passes the correct value back
-*
-*
-* */
-((global) => {
- describe('Subbable Resource', function () {
- describe('PubSub', function () {
- beforeEach(function () {
- this.MockResource = new global.SubbableResource('https://example.com');
- });
- it('should successfully add a single subscriber', function () {
- const callback = () => {};
- this.MockResource.subscribe(callback);
-
- expect(this.MockResource.subscribers.length).toBe(1);
- expect(this.MockResource.subscribers[0]).toBe(callback);
- });
-
- it('should successfully add multiple subscribers', function () {
- const callbackOne = () => {};
- const callbackTwo = () => {};
- const callbackThree = () => {};
-
- this.MockResource.subscribe(callbackOne);
- this.MockResource.subscribe(callbackTwo);
- this.MockResource.subscribe(callbackThree);
-
- expect(this.MockResource.subscribers.length).toBe(3);
- });
-
- it('should successfully publish an update to a single subscriber', function () {
- const state = { myprop: 1 };
-
- const callbacks = {
- one: (data) => expect(data.myprop).toBe(2),
- two: (data) => expect(data.myprop).toBe(2),
- three: (data) => expect(data.myprop).toBe(2)
- };
-
- const spyOne = spyOn(callbacks, 'one');
- const spyTwo = spyOn(callbacks, 'two');
- const spyThree = spyOn(callbacks, 'three');
-
- this.MockResource.subscribe(callbacks.one);
- this.MockResource.subscribe(callbacks.two);
- this.MockResource.subscribe(callbacks.three);
-
- state.myprop += 1;
-
- this.MockResource.publish(state);
-
- expect(spyOne).toHaveBeenCalled();
- expect(spyTwo).toHaveBeenCalled();
- expect(spyThree).toHaveBeenCalled();
- });
- });
- });
-})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 436f7064a69..cea223bd243 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
-/*= require syntax_highlight */
+require('~/syntax_highlight');
(function() {
describe('Syntax Highlighter', function() {
@@ -41,4 +41,4 @@
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
new file mode 100644
index 00000000000..fae462561e9
--- /dev/null
+++ b/spec/javascripts/test_bundle.js
@@ -0,0 +1,68 @@
+// enable test fixtures
+require('jasmine-jquery');
+
+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'));
+
+// stub expected globals
+window.gl = window.gl || {};
+window.gl.TEST_HOST = 'http://test.host';
+window.gon = window.gon || {};
+
+// render all of our tests
+const testsContext = require.context('.', true, /_spec$/);
+testsContext.keys().forEach(function (path) {
+ try {
+ testsContext(path);
+ } catch (err) {
+ console.error('[ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function () {
+ it(`includes '${path}'`, function () {
+ expect(err).toBeNull();
+ });
+ });
+ }
+});
+
+// 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
+ const troubleMakers = [
+ './blob_edit/blob_edit_bundle.js',
+ './cycle_analytics/components/stage_plan_component.js',
+ './cycle_analytics/components/stage_staging_component.js',
+ './cycle_analytics/components/stage_test_component.js',
+ './diff_notes/components/jump_to_discussion.js',
+ './diff_notes/components/resolve_count.js',
+ './merge_conflicts/components/inline_conflict_lines.js',
+ './merge_conflicts/components/parallel_conflict_lines.js',
+ './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;
+ }
+
+ it(`includes '${path}'`, function () {
+ try {
+ sourceFiles(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
+ }
+ });
+ });
+});
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
new file mode 100644
index 00000000000..66e4fbd6304
--- /dev/null
+++ b/spec/javascripts/todos_spec.js
@@ -0,0 +1,63 @@
+require('~/todos');
+require('~/lib/utils/common_utils');
+
+describe('Todos', () => {
+ preloadFixtures('todos/todos.html.raw');
+ let todoItem;
+
+ beforeEach(() => {
+ loadFixtures('todos/todos.html.raw');
+ todoItem = document.querySelector('.todos-list .todo');
+
+ return new gl.Todos();
+ });
+
+ describe('goToTodoUrl', () => {
+ it('opens the todo url', (done) => {
+ const todoLink = todoItem.dataset.url;
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(todoLink);
+ done();
+ });
+
+ todoItem.click();
+ });
+
+ describe('meta click', () => {
+ let visitUrlSpy;
+
+ beforeEach(() => {
+ spyOn(gl.utils, 'isMetaClick').and.returnValue(true);
+ visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {});
+ });
+
+ it('opens the todo url in another tab', (done) => {
+ const todoLink = todoItem.dataset.url;
+
+ spyOn(window, 'open').and.callFake((url, target) => {
+ expect(todoLink).toEqual(url);
+ expect(target).toEqual('_blank');
+ done();
+ });
+
+ todoItem.click();
+ expect(visitUrlSpy).not.toHaveBeenCalled();
+ });
+
+ it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => {
+ const avatarImage = todoItem.querySelector('img');
+ const avatarUrl = avatarImage.parentElement.getAttribute('href');
+
+ spyOn(window, 'open').and.callFake((url, target) => {
+ expect(avatarUrl).toEqual(url);
+ expect(target).toEqual('_blank');
+ done();
+ });
+
+ avatarImage.click();
+ expect(visitUrlSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 80163fd72d3..af2d02b6b29 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -2,11 +2,11 @@
/* global MockU2FDevice */
/* global U2FAuthenticate */
-/*= require u2f/authenticate */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/authenticate');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FAuthenticate', function() {
@@ -25,19 +25,20 @@
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form')
);
+
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+
return this.component.start();
});
it('allows authenticating via a U2F device', function() {
- var authenticatedMessage, deviceResponse, inProgressMessage;
+ var inProgressMessage;
inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
this.u2fDevice.respondToAuthenticateRequest({
deviceData: "this is data from the device"
});
- authenticatedMessage = this.container.find("p");
- deviceResponse = this.container.find('#js-device-response');
- expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.');
- return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
return describe("errors", function() {
it("displays an error message", function() {
@@ -51,7 +52,7 @@
return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
});
return it("allows retrying authentication after an error", function() {
- var authenticatedMessage, retryButton, setupButton;
+ var retryButton, setupButton;
setupButton = this.container.find("#js-login-u2f-device");
setupButton.trigger('click');
this.u2fDevice.respondToAuthenticateRequest({
@@ -64,9 +65,8 @@
this.u2fDevice.respondToAuthenticateRequest({
deviceData: "this is data from the device"
});
- authenticatedMessage = this.container.find("p");
- return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated.");
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 287bfb4138b..6677fe9c1ee 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -30,4 +30,4 @@
return MockU2FDevice;
})();
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 0790553b67e..0f390c8b980 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -2,11 +2,11 @@
/* global MockU2FDevice */
/* global U2FRegister */
-/*= require u2f/register */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/register');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FRegister', function() {
@@ -74,4 +74,4 @@
});
});
});
-}).call(this);
+}).call(window);
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 b/spec/javascripts/visibility_select_spec.js
new file mode 100644
index 00000000000..9727c03c91e
--- /dev/null
+++ b/spec/javascripts/visibility_select_spec.js
@@ -0,0 +1,100 @@
+require('~/visibility_select');
+
+(() => {
+ const VisibilitySelect = gl.VisibilitySelect;
+
+ describe('VisibilitySelect', function () {
+ const lockedElement = document.createElement('div');
+ lockedElement.dataset.helpBlock = 'lockedHelpBlock';
+
+ const checkedElement = document.createElement('div');
+ checkedElement.dataset.description = 'checkedDescription';
+
+ const mockElements = {
+ container: document.createElement('div'),
+ select: document.createElement('div'),
+ '.help-block': document.createElement('div'),
+ '.js-locked': lockedElement,
+ 'option:checked': checkedElement,
+ };
+
+ beforeEach(function () {
+ spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
+ });
+
+ describe('#constructor', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('sets the container member', function () {
+ expect(this.visibilitySelect.container).toEqual(mockElements.container);
+ });
+
+ it('queries and sets the helpBlock member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block');
+ expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']);
+ });
+
+ it('queries and sets the select member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('select');
+ expect(this.visibilitySelect.select).toEqual(mockElements.select);
+ });
+
+ describe('if there is no container element provided', function () {
+ it('throws an error', function () {
+ expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1');
+ });
+ });
+ });
+
+ describe('#init', function () {
+ describe('if there is a select', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('calls updateHelpText', function () {
+ spyOn(VisibilitySelect.prototype, 'updateHelpText');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled();
+ });
+
+ it('adds a change event listener', function () {
+ spyOn(this.visibilitySelect.select, 'addEventListener');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change');
+ });
+ });
+
+ describe('if there is no select', function () {
+ beforeEach(function () {
+ mockElements.select = undefined;
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the locked `data-help-block` messaged', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(lockedElement.dataset.helpBlock);
+ });
+
+ afterEach(function () {
+ mockElements.select = document.createElement('div');
+ });
+ });
+ });
+
+ describe('#updateHelpText', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the selected options `data-description`', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(checkedElement.dataset.description);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6
deleted file mode 100644
index b21f6912e06..00000000000
--- a/spec/javascripts/visibility_select_spec.js.es6
+++ /dev/null
@@ -1,100 +0,0 @@
-/*= require visibility_select */
-
-(() => {
- const VisibilitySelect = gl.VisibilitySelect;
-
- describe('VisibilitySelect', function () {
- const lockedElement = document.createElement('div');
- lockedElement.dataset.helpBlock = 'lockedHelpBlock';
-
- const checkedElement = document.createElement('div');
- checkedElement.dataset.description = 'checkedDescription';
-
- const mockElements = {
- container: document.createElement('div'),
- select: document.createElement('div'),
- '.help-block': document.createElement('div'),
- '.js-locked': lockedElement,
- 'option:checked': checkedElement,
- };
-
- beforeEach(function () {
- spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
- });
-
- describe('#constructor', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- });
-
- it('sets the container member', function () {
- expect(this.visibilitySelect.container).toEqual(mockElements.container);
- });
-
- it('queries and sets the helpBlock member', function () {
- expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block');
- expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']);
- });
-
- it('queries and sets the select member', function () {
- expect(Element.prototype.querySelector).toHaveBeenCalledWith('select');
- expect(this.visibilitySelect.select).toEqual(mockElements.select);
- });
-
- describe('if there is no container element provided', function () {
- it('throws an error', function () {
- expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1');
- });
- });
- });
-
- describe('#init', function () {
- describe('if there is a select', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- });
-
- it('calls updateHelpText', function () {
- spyOn(VisibilitySelect.prototype, 'updateHelpText');
- this.visibilitySelect.init();
- expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled();
- });
-
- it('adds a change event listener', function () {
- spyOn(this.visibilitySelect.select, 'addEventListener');
- this.visibilitySelect.init();
- expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change');
- });
- });
-
- describe('if there is no select', function () {
- beforeEach(function () {
- mockElements.select = undefined;
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- this.visibilitySelect.init();
- });
-
- it('updates the helpBlock text to the locked `data-help-block` messaged', function () {
- expect(this.visibilitySelect.helpBlock.textContent)
- .toEqual(lockedElement.dataset.helpBlock);
- });
-
- afterEach(function () {
- mockElements.select = document.createElement('div');
- });
- });
- });
-
- describe('#updateHelpText', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- this.visibilitySelect.init();
- });
-
- it('updates the helpBlock text to the selected options `data-description`', function () {
- expect(this.visibilitySelect.helpBlock.textContent)
- .toEqual(checkedElement.dataset.description);
- });
- });
- });
-})();
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6
deleted file mode 100644
index d6c6f786fb1..00000000000
--- a/spec/javascripts/vue_common_components/commit_spec.js.es6
+++ /dev/null
@@ -1,131 +0,0 @@
-//= require vue_common_component/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_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6
deleted file mode 100644
index 1a7f2bb5fb8..00000000000
--- a/spec/javascripts/vue_pagination/pagination_spec.js.es6
+++ /dev/null
@@ -1,168 +0,0 @@
-//= require vue
-//= require lib/utils/common_utils
-//= require vue_pagination/index
-/* global fixture, gl */
-
-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', () => {
- fixture.set('<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('all');
- });
-
- it('should go to the previous page', () => {
- fixture.set('<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('all');
- });
-
- it('should go to the next page', () => {
- fixture.set('<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('all');
- });
-
- it('should go to the last page', () => {
- fixture.set('<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('all');
- });
-
- it('should go to the first page', () => {
- fixture.set('<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('all');
- });
-
- it('should do nothing', () => {
- fixture.set('<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('all');
- });
-});
-
-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/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
new file mode 100644
index 00000000000..15ab10b9b69
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -0,0 +1,131 @@
+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..412abfd5e41
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -0,0 +1,87 @@
+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..54d81e2ea7d
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -0,0 +1,64 @@
+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..9cb067921a7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -0,0 +1,158 @@
+require('~/lib/utils/common_utils');
+require('~/vue_shared/components/table_pagination');
+
+describe('Pagination component', () => {
+ let component;
+
+ const changeChanges = {
+ one: '',
+ };
+
+ const change = (one) => {
+ changeChanges.one = one;
+ };
+
+ 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);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+});
+
+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/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index be706ca304f..99515f2e5f2 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -3,7 +3,7 @@
/* global Mousetrap */
/* global ZenMode */
-/*= require zen_mode */
+require('~/zen_mode');
(function() {
var enterZen, escapeKeydown, exitZen;
@@ -76,4 +76,4 @@
keyCode: 27
}));
};
-}).call(this);
+}).call(window);
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb
new file mode 100644
index 00000000000..580450eef1e
--- /dev/null
+++ b/spec/lib/additional_email_headers_interceptor_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe AdditionalEmailHeadersInterceptor do
+ it 'adds Auto-Submitted header' do
+ mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
+
+ expect(mail.header['To'].value).to eq('test@mail.com')
+ expect(mail.header['From'].value).to eq('info@mail.com')
+ expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
+ expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
+ end
+end
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 81b9a513ce3..deaabceef1c 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do
it 'returns the referenced project' do
project2 = double('referenced project')
- expect(Project).to receive(:find_with_namespace).
+ expect(Project).to receive(:find_by_full_path).
with('cross/reference').and_return(project2)
expect(project_from_ref('cross/reference')).to eq project2
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/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 456dbac0698..11607d4fb26 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
- describe '#issues_per_Project' do
+ describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
@@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
end
+
+ describe '.references_in' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'yields valid references' do
+ expect do |b|
+ described_class.references_in(issue.to_reference, &b)
+ end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData)
+ end
+
+ it "doesn't yield invalid references" do
+ expect do |b|
+ described_class.references_in('#0', &b)
+ end.not_to yield_control
+ end
+
+ it "doesn't yield unsupported references" do
+ expect do |b|
+ described_class.references_in(merge_request.to_reference, &b)
+ end.not_to yield_control
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
new file mode 100644
index 00000000000..f85a5dcbd8b
--- /dev/null
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Banzai::Filter::PlantumlFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'should replace plantuml pre tag with img tag' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if disabled' do
+ stub_application_setting(plantuml_enabled: false)
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<pre class="plantuml"><code>Bob -&gt; Sara : Hello</code><pre></pre></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if url is invalid' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ 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/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 3e1ac9fb2b2..9873774909e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -112,6 +112,25 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
+ context 'mentioning a nested group' do
+ it_behaves_like 'a reference containing an element node'
+
+ let(:group) { create(:group, :nested) }
+ let(:reference) { group.to_reference }
+
+ it 'links to the nested group' do
+ doc = reference_filter("Hey #{reference}")
+
+ 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
doc = reference_filter("Mention me (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
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 49349035b3b..53abc056602 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -4,28 +4,79 @@ module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
+ describe 'our current .gitlab-ci.yml' do
+ let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
+
+ it 'is valid' do
+ error_message = described_class.validation_message(config)
+
+ expect(error_message).to be_nil
+ end
+ end
+
describe '#build_attributes' do
- context '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',
+ coverage: '/Code coverage: \d+\.\d+/' })
+ end
+
+ it 'includes coverage regexp in build attributes' do
+ expect(subject)
+ .to include(coverage_regex: 'Code coverage: \d+\.\d+')
+ end
+ end
+ end
- let(:config_base) { { rspec: { script: "rspec" } } }
- let(:config) { YAML.dump(config_base) }
+ 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
- context 'when config has coverage set at the global scope' do
- before do
- config_base.update(coverage: '/\(\d+\.\d+\) covered/')
+ 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
- context "and 'rspec' job doesn't have coverage set" do
- it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') }
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
end
- context "but 'rspec' job also has coverage set" do
- before do
- config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/'
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
end
- it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') }
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
end
end
end
@@ -95,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)
@@ -172,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"] },
})
@@ -251,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)
@@ -579,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
@@ -908,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
@@ -1214,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")
@@ -1256,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")
@@ -1319,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/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index e3066311b7d..d70690f589d 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -5,15 +5,15 @@ describe EventFilter, lib: true do
let(:source_user) { create(:user) }
let!(:public_project) { create(:empty_project, :public) }
- let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
- let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
- let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) }
- let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) }
- let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) }
- let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) }
- let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
- let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
- let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+ let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
+ let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
+ let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
+ let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
+ let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) }
+ let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) }
+ let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) }
+ let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) }
+ let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) }
it 'applies push filter' do
events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90bc7dad379..730ca1f7c0a 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',
result: 'review/feature/add-review-apps',
variables: [
{ key: 'CI_BUILD_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/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index ba199917f5c..bca57105d1d 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -41,6 +41,29 @@ module Gitlab
render(input, context, asciidoc_opts)
end
end
+
+ context "XSS" do
+ links = {
+ 'links' => {
+ input: 'link:mylink"onmouseover="alert(1)[Click Here]',
+ output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
+ },
+ 'images' => {
+ input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
+ output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>"
+ },
+ 'pre' => {
+ input: '```mypre"><script>alert(3)</script>',
+ output: "<div>\n<div>\n<pre lang=\"mypre\">\"&gt;<code></code></pre>\n</div>\n</div>"
+ }
+ }
+
+ links.each do |name, data|
+ it "does not convert dangerous #{name} into HTML" do
+ expect(render(data[:input], context)).to eql data[:output]
+ end
+ end
+ end
end
def render(*args)
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/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
index 0cf18514251..63c7ca5a915 100644
--- a/spec/lib/gitlab/badge/shared/metadata.rb
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -18,4 +18,14 @@ shared_examples 'badge metadata' do
it { is_expected.to include metadata.image_url }
it { is_expected.to include metadata.link_url }
end
+
+ describe '#to_asciidoc' do
+ subject { metadata.to_asciidoc }
+
+ it { is_expected.to include metadata.image_url }
+ it { is_expected.to include metadata.link_url }
+ it { is_expected.to include 'image:' }
+ it { is_expected.to include 'link=' }
+ it { is_expected.to include 'title=' }
+ 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/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 d4f1780b174..684d01e9056 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -10,10 +10,10 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when filtering all the entry/node names' do
it 'contains the expected node names' do
- node_names = described_class.nodes.keys
- expect(node_names).to match_array(%i[before_script image services
- after_script variables stages
- types cache coverage])
+ expect(described_class.nodes.keys)
+ .to match_array(%i[before_script image services
+ after_script variables stages
+ types cache])
end
end
end
@@ -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' } }
@@ -40,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do
end
it 'creates node object for each entry' do
- expect(global.descendants.count).to eq 9
+ expect(global.descendants.count).to eq 8
end
it 'creates node object using valid class' do
@@ -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
@@ -181,12 +183,12 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#nodes' do
it 'instantizes all nodes' do
- expect(global.descendants.count).to eq 9
+ expect(global.descendants.count).to eq 8
end
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/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 01b2a55b63c..e18a219ef36 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::ContributionsCalendar do
end
let(:feature_project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+ create(:empty_project, :public, :issues_private) do |project|
create(:project_member, user: contributor, project: project).project
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
new file mode 100644
index 00000000000..c455cd9b942
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::BaseEventFetcher do
+ let(:max_events) { 2 }
+ let(:project) { create(:project) }
+ let(:user) { create(:user, :admin) }
+ let(:start_time_attrs) { Issue.arel_table[:created_at] }
+ let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] }
+ let(:options) do
+ { start_time_attrs: start_time_attrs,
+ end_time_attrs: end_time_attrs,
+ from: 30.days.ago }
+ end
+
+ subject do
+ described_class.new(project: project,
+ stage: :issue,
+ options: options).fetch
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event|
+ event
+ end
+
+ stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events)
+
+ setup_events(count: 3)
+ end
+
+ it 'limits the rows to the max number' do
+ expect(subject.count).to eq(max_events)
+ end
+
+ def setup_events(count:)
+ count.times do
+ issue = create(:issue, project: project, created_at: 2.days.ago)
+ milestone = create(:milestone, project: project)
+
+ issue.update(milestone: milestone)
+ create_merge_request_closing_issue(issue)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 6c71e98066b..91c43f2bdc0 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do
it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+
+ context 'commit author_url' do
+ context 'when no commit present' do
+ let(:build) { create(:ci_build) }
+
+ it 'sets to mailing address of git_author_email' do
+ expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ end
+ end
+
+ context 'when commit present but has no author' do
+ let(:build) { create(:ci_build, :with_commit) }
+
+ it 'sets to mailing address of git_author_email' do
+ expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ end
+ end
+
+ context 'when commit and author are present' do
+ let(:build) { create(:ci_build, :with_commit_and_author) }
+
+ it 'sets to GitLab user url' do
+ expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username))
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 7fd25b9e5bf..e007044868c 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#add_concurrent_index' do
context 'outside a transaction' do
before do
- expect(model).to receive(:transaction_open?).and_return(false)
-
- unless Gitlab::Database.postgresql?
- allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout)
- end
+ allow(model).to receive(:transaction_open?).and_return(false)
end
context 'using PostgreSQL' do
- before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) }
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(model).to receive(:disable_statement_timeout)
+ end
it 'creates the index concurrently' do
expect(model).to receive(:add_index).
@@ -59,6 +58,81 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#add_concurrent_foreign_key' do
+ context 'inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using MySQL' do
+ it 'creates a regular foreign key' do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+
+ expect(model).to receive(:add_foreign_key).
+ with(:projects, :users, column: :user_id, on_delete: :cascade)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
+ end
+
+ context 'using PostgreSQL' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ it 'creates a concurrent foreign key' do
+ expect(model).to receive(:disable_statement_timeout)
+ expect(model).to receive(:execute).ordered.with(/NOT VALID/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
+ end
+ end
+ end
+
+ describe '#concurrent_foreign_key_name' do
+ it 'returns the name for a foreign key' do
+ name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name,
+ :with_a_very_long_column_name)
+
+ expect(name).to be_an_instance_of(String)
+ expect(name.length).to eq(13)
+ end
+ end
+
+ describe '#disable_statement_timeout' do
+ context 'using PostgreSQL' do
+ it 'disables statement timeouts' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:execute).with('SET statement_timeout TO 0')
+
+ model.disable_statement_timeout
+ end
+ end
+
+ context 'using MySQL' do
+ it 'does nothing' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).not_to receive(:execute)
+
+ model.disable_statement_timeout
+ end
+ end
+ end
+
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 3031559c613..edd01d032c8 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -5,6 +5,12 @@ class MigrationTest
end
describe Gitlab::Database, lib: true do
+ describe '.adapter_name' do
+ it 'returns the name of the adapter' do
+ expect(described_class.adapter_name).to be_an_instance_of(String)
+ end
+ end
+
# These are just simple smoke tests to check if the methods work (regardless
# of what they may return).
describe '.mysql?' do
@@ -55,6 +61,85 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_first_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'}
+ end
+ end
+
+ describe '.with_connection_pool' do
+ it 'creates a new connection pool and disconnect it after used' do
+ closed_pool = nil
+
+ described_class.with_connection_pool(1) do |pool|
+ pool.with_connection do |connection|
+ connection.execute('SELECT 1 AS value')
+ end
+
+ expect(pool).to be_connected
+
+ closed_pool = pool
+ end
+
+ expect(closed_pool).not_to be_connected
+ end
+
+ it 'disconnects the pool even an exception was raised' do
+ error = Class.new(RuntimeError)
+ closed_pool = nil
+
+ begin
+ described_class.with_connection_pool(1) do |pool|
+ pool.with_connection do |connection|
+ connection.execute('SELECT 1 AS value')
+ end
+
+ closed_pool = pool
+
+ raise error.new('boom')
+ end
+ rescue error
+ end
+
+ expect(closed_pool).not_to be_connected
+ end
+ end
+
+ describe '.create_connection_pool' do
+ it 'creates a new connection pool with specific pool size' do
+ pool = described_class.create_connection_pool(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
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 5893485634d..0e9309d278e 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -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/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index f5822fed37c..994995b57b8 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::CreateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Create file",
file_path: file_name,
@@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::UpdateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Update file",
file_path: file_name,
@@ -122,10 +122,10 @@ describe Gitlab::Diff::PositionTracer, lib: true do
end
def delete_file(branch_name, file_name)
- Files::DeleteService.new(
+ Files::DestroyService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Delete file",
file_path: file_name
@@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do
}
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request, options)
+
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 17a4ef25210..b300feaabe1 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it_behaves_like 'an email that contains a mail key', 'References'
end
+
+ context 'mail key is in the References header with a comma' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') }
+
+ it_behaves_like 'an email that contains a mail key', 'References'
+ end
end
end
end
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/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..bc139d5ef28 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,63 @@ 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
+ 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 +161,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 +178,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 +214,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 +278,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
@@ -529,7 +543,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 +552,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 +783,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 +824,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 +948,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 b080be62b34..48f7754bed8 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -94,8 +94,6 @@ describe Gitlab::GitAccess, lib: true do
context 'when repository is enabled' do
it 'give access to download code' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
-
expect(subject.allowed?).to be_truthy
end
end
@@ -201,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
@@ -209,7 +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(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false)
+ source_branch = project.repository.create_file(
+ user,
+ FFaker::InternetSE.login_user_name,
+ FFaker::HipsterIpsum.paragraph,
+ message: FFaker::HipsterIpsum.sentence,
+ branch_name: unprotected_branch)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
@@ -228,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
@@ -297,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_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4a0cdc6887e..1ae293416e4 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -36,8 +36,6 @@ describe Gitlab::GitAccessWiki, lib: true do
context 'when wiki feature is enabled' do
it 'give access to download wiki code' do
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
-
expect(subject.allowed?).to be_truthy
end
end
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/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/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index e6e33d3686a..cc38872e426 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Gitlab::GithubImport::CommentFormatter, lib: true do
+ let(:client) { double }
let(:project) { create(:empty_project) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
let(:base) do
@@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
}
end
- subject(:comment) { described_class.new(project, raw)}
+ subject(:comment) { described_class.new(project, raw, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
describe '#attributes' do
context 'when do not reference a portion of the diff' do
@@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
context 'when author is a GitLab user' do
let(:raw) { double(base.merge(user: octocat)) }
- it 'returns GitLab user id as author_id' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 72421832ffc..8b867fbe322 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+ allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat)
allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
@@ -53,9 +54,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end
- let(:octocat) { double(id: 123456, login: 'octocat') }
- 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',
@@ -125,31 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
- 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',
@@ -174,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
@@ -202,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do
end
end
- let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) }
+ 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
@@ -220,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{}
)
- described_class.new(project).client
+ subject.client
end
end
end
@@ -228,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
@@ -236,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
@@ -245,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/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index eec1fabab54..f34d09f2c1d 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Gitlab::GithubImport::IssueFormatter, lib: true do
+ let(:client) { double }
let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
+ 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') }
@@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
}
end
- subject(:issue) { described_class.new(project, raw_data) }
+ subject(:issue) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
context 'when issue is open' do
@@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
expect(issue.attributes.fetch(:assignee_id)).to be_nil
end
- it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
end
context 'when it has a milestone' do
@@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) }
- it 'returns project#creator_id as author_id when is not a GitLab user' do
+ it 'returns project creator_id as author_id when is not a GitLab user' do
expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
end
- it 'returns GitLab user id as author_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
end
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
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 90947ff4707..44423917944 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -1,16 +1,19 @@
require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
+ let(:client) { double }
let(:project) { create(:project, :repository) }
let(:source_sha) { create(:commit, project: project).id }
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(:octocat) { double(id: 123456, login: 'octocat') }
+ 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') }
let(:base_data) do
@@ -32,7 +35,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
}
end
- subject(:pull_request) { described_class.new(project, raw_data) }
+ subject(:pull_request) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do
@@ -44,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',
@@ -70,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',
@@ -97,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',
@@ -121,26 +128,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
end
- it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
end
context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) }
- it 'returns project#creator_id as author_id when is not a GitLab user' do
+ it 'returns project creator_id as author_id when is not a GitLab user' do
expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
end
- it 'returns GitLab user id as author_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
end
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
@@ -177,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
@@ -188,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
@@ -254,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) }
@@ -261,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/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb
new file mode 100644
index 00000000000..db792233657
--- /dev/null
+++ b/spec/lib/gitlab/github_import/user_formatter_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::UserFormatter, lib: true do
+ let(:client) { double }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+
+ subject(:user) { described_class.new(client, octocat) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ describe '#gitlab_id' do
+ context 'when GitHub user is a GitLab user' do
+ it 'return GitLab user id when user associated their account with GitHub' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when user primary email matches GitHub email' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when any of user linked emails matches GitHub email' do
+ gl_user = create(:user, email: 'johndoe@example.com')
+ create(:email, user: gl_user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+ end
+
+ it 'returns nil when GitHub user is not a GitLab user' do
+ expect(user.gitlab_id).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 20241d4d63e..e47956a365f 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
@@ -135,6 +136,7 @@ project:
- slack_slash_commands_service
- irker_service
- pivotaltracker_service
+- prometheus_service
- hipchat_service
- flowdock_service
- assembla_service
@@ -153,6 +155,7 @@ project:
- gitlab_issue_tracker_service
- external_wiki_service
- kubernetes_service
+- mock_ci_service
- forked_project_link
- forked_from_project
- forked_project_links
@@ -192,15 +195,18 @@ project:
- environments
- deployments
- project_feature
+- pages_domains
- authorized_users
- project_authorizations
- route
- statistics
+- uploads
award_emoji:
- awardable
- user
priorities:
- label
timelogs:
-- trackable
+- issue
+- merge_request
- 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 53f7d244d88..f3fd0d82875 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -2,14 +2,15 @@ require 'spec_helper'
describe Gitlab::ImportExport, services: true do
describe 'export filename' do
- let(:project) { create(:empty_project, :public, path: 'project-path') }
+ let(:group) { create(:group, :nested) }
+ let(:project) { create(:empty_project, :public, path: 'project-path', namespace: group) }
it 'contains the project path' do
expect(described_class.export_filename(project: project)).to include(project.path)
end
it 'contains the namespace path' do
- expect(described_class.export_filename(project: project)).to include(project.namespace.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/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index f2cb028206f..b9d4e59e770 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -116,5 +116,27 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
end
end
+
+ context 'importing group members' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ user.update(email: 'invite@test.com')
+ end
+
+ it 'maps the importer' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+
+ it 'maps the group member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
end
end
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..c3d5c451a3c 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,
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
new file mode 100644
index 00000000000..a78836c3c34
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -0,0 +1,48 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "visibility_level": 10,
+ "archived": false,
+ "labels": [
+ {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ ]
+ },
+ {
+ "id": 3,
+ "title": "test3",
+ "color": "#428bca",
+ "group_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "project_id": null,
+ "type": "GroupLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "hooks": [
+
+ ]
+} \ 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 40d7d59f03b..f4a21c24fa1 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -3,24 +3,24 @@ include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' do
- let(:user) { create(:user) }
- let(:namespace) { create(:namespace, owner: user) }
- let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
- let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
- let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
- let(:restored_project_json) { project_tree_restorer.restore }
+ before(:context) do
+ @user = create(:user)
- before do
- allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ RSpec::Mocks.with_temporary_scope do
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path')
+ allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
+ project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
+ @restored_project_json = project_tree_restorer.restore
+ end
end
context 'JSON' do
it 'restores models based on JSON' do
- expect(restored_project_json).to be true
+ expect(@restored_project_json).to be true
end
it 'restore correct project features' do
- restored_project_json
project = Project.find_by_path('project')
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
@@ -31,62 +31,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
it 'has the same label associated to two issues' do
- restored_project_json
-
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
- restored_project_json
-
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
it 'creates a valid pipeline note' do
- restored_project_json
-
expect(Ci::Pipeline.first.notes).not_to be_empty
end
it 'restores pipelines with missing ref' do
- restored_project_json
-
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
it 'restores the correct event with symbolised data' do
- restored_project_json
-
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end
it 'preserves updated_at on issues' do
- restored_project_json
-
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
it 'contains the merge access levels on a protected branch' do
- restored_project_json
-
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
it 'contains the push access levels on a protected branch' do
- restored_project_json
-
expect(ProtectedBranch.first.push_access_levels).not_to be_empty
end
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
- before do
- restored_project_json
- end
-
it 'restores the event' do
expect(event).not_to be_nil
end
@@ -99,77 +79,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the correct data for merge request st_diffs' do
# makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
- expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9)
+ expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9)
end
it 'has labels associated to label links, associated to issues' do
- restored_project_json
-
expect(Label.first.label_links.first.target).not_to be_nil
end
it 'has project labels' do
- restored_project_json
-
expect(ProjectLabel.count).to eq(2)
end
it 'has no group labels' do
- restored_project_json
-
expect(GroupLabel.count).to eq(0)
end
- context 'with group' do
- let!(:project) do
- create(:empty_project,
- name: 'project',
- path: 'project',
- builds_access_level: ProjectFeature::DISABLED,
- issues_access_level: ProjectFeature::DISABLED,
- group: create(:group))
- end
-
- it 'has group labels' do
- restored_project_json
-
- expect(GroupLabel.count).to eq(1)
- end
-
- it 'has label priorities' do
- restored_project_json
-
- expect(GroupLabel.first.priorities).not_to be_empty
- end
- end
-
it 'has a project feature' do
- restored_project_json
-
- expect(project.project_feature).not_to be_nil
+ expect(@project.project_feature).not_to be_nil
end
it 'restores the correct service' do
- restored_project_json
-
expect(CustomIssueTrackerService.first).not_to be_nil
end
context 'Merge requests' do
- before do
- restored_project_json
- end
-
it 'always has the new project as a target' do
- expect(MergeRequest.find_by_title('MR1').target_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
end
it 'has the same source project as originally if source/target are the same' do
- expect(MergeRequest.find_by_title('MR1').source_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project)
end
it 'has the new project as target if source/target differ' do
- expect(MergeRequest.find_by_title('MR2').target_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project)
end
it 'has no source if source/target differ' do
@@ -177,39 +120,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
end
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
-
- restored_project_json
+ context 'tokens are regenerated' do
+ it 'has a new CI trigger token' do
+ expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
+ end
- expect(shared.errors.first).not_to include('test')
- end
+ it 'has a new CI build token' do
+ expect(Ci::Build.where(token: 'abcd')).to be_empty
end
end
+ end
+ end
- context 'when there is an existing build with build token' do
- it 'restores project json correctly' do
- create(:ci_build, token: 'abcd')
+ context 'Light JSON' do
+ let(:user) { create(:user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
+ let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+ let(:restored_project_json) { project_tree_restorer.restore }
- expect(restored_project_json).to be true
- end
- end
+ before do
+ allow(ImportExport).to receive(:project_filename).and_return('project.light.json')
+ allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ end
+
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
- context 'tokens are regenerated' do
- before do
restored_project_json
- end
- it 'has a new CI trigger token' do
- expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
+ expect(shared.errors.first).not_to include('test')
end
+ end
+ end
- it 'has a new CI build token' do
- expect(Ci::Build.where(token: 'abcd')).to be_empty
- end
+ context 'when there is an existing build with build token' do
+ it 'restores project json correctly' do
+ create(:ci_build, token: 'abcd')
+
+ expect(restored_project_json).to be true
+ end
+ end
+
+ context 'with group' do
+ let!(:project) do
+ create(:empty_project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ before do
+ restored_project_json
+ end
+
+ it 'has group labels' do
+ expect(GroupLabel.count).to eq(1)
+ end
+
+ it 'has label priorities' do
+ expect(GroupLabel.first.priorities).not_to be_empty
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 d480c3821ec..012c22ec5ad 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
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, shared: shared) }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
@@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has pipeline builds' do
- expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
+ expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1)
end
it 'has pipeline commits' do
@@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has project and group labels' do
- label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
+ 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
- priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+ priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities'] }
expect(priorities.flatten).not_to be_empty
end
@@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(project_tree_saver.save).to be true
end
+
+ context 'group members' do
+ let(:user2) { create(:user, email: 'group@member.com') }
+ let(:member_emails) do
+ saved_project_json['project_members'].map do |pm|
+ pm['user']['email']
+ end
+ end
+
+ before do
+ Group.first.add_developer(user2)
+ end
+
+ it 'does not export group members if it has no permission' do
+ Group.first.add_developer(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'does not export group members as master' do
+ Group.first.add_master(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'exports group members as group owner' do
+ Group.first.add_owner(user)
+
+ expect(member_emails).to include('group@member.com')
+ end
+
+ context 'as admin' do
+ let(:user) { create(:admin) }
+
+ it 'exports group members as admin' do
+ expect(member_emails).to include('group@member.com')
+ end
+
+ it 'exports group members as project members' do
+ member_types = saved_project_json['project_members'].map { |pm| pm['source_type'] }
+
+ expect(member_types).to all(eq('Project'))
+ end
+ end
+ end
end
end
@@ -152,6 +197,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project = create(:project,
:public,
:repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
issues: [issue],
snippets: [snippet],
releases: [release],
@@ -167,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_status = create(:commit_status, project: project)
ci_pipeline = create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- statuses: [commit_status])
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
create(:ci_build, pipeline: ci_pipeline, project: project)
create(:milestone, project: project)
@@ -182,13 +230,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
-
project
end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index 3ceb1e7e803..48d74b07e27 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -86,6 +86,10 @@ describe Gitlab::ImportExport::Reader, lib: true do
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
end
+ it 'generates the correct hash for group members' do
+ expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } })
+ end
+
def setup_yaml(hash)
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
end
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/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
new file mode 100644
index 00000000000..168a59e5139
--- /dev/null
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RepoRestorer, services: true do
+ describe 'bundle a project Git repo' do
+ let(:user) { create(:user) }
+ let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
+ let!(:project) { create(:empty_project) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
+ let(:restorer) do
+ described_class.new(path_to_bundle: bundle_path,
+ shared: shared,
+ project: project)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ bundler.save
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
+
+ it 'restores the repo successfully' do
+ expect(restorer.restore).to be true
+ end
+
+ it 'has the webhooks' do
+ restorer.restore
+
+ expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 95b230e4f5c..c718e792461 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -21,6 +21,7 @@ Issue:
- milestone_id
- weight
- time_estimate
+- relative_position
Event:
- id
- target_type
@@ -142,7 +143,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
@@ -240,6 +241,8 @@ Ci::Trigger:
- created_at
- updated_at
- gl_project_id
+- owner_id
+- description
DeployKey:
- id
- user_id
@@ -350,8 +353,8 @@ LabelPriority:
Timelog:
- id
- time_spent
-- trackable_id
-- trackable_type
+- merge_request_id
+- issue_id
- user_id
- created_at
- updated_at
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/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 7e951e3fcdd..698bd72d0f8 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -90,4 +90,19 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
end
end
+
+ context 'self.scan_fallback_references' do
+ let(:references) do
+ '<issue_1@localhost>' +
+ ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' +
+ ',<exchange@microsoft.com>'
+ end
+
+ it 'returns reply key' do
+ expect(described_class.scan_fallback_references(references))
+ .to eq(%w[issue_1@localhost
+ reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
+ exchange@microsoft.com])
+ end
+ end
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/other_markup.rb b/spec/lib/gitlab/other_markup.rb
new file mode 100644
index 00000000000..8f5a353b381
--- /dev/null
+++ b/spec/lib/gitlab/other_markup.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::OtherMarkup, lib: true do
+ context "XSS Checks" do
+ links = {
+ 'links' => {
+ file: 'file.rdoc',
+ input: 'XSS[JaVaScriPt:alert(1)]',
+ output: '<p><a>XSS</a></p>'
+ }
+ }
+ links.each do |name, data|
+ it "does not convert dangerous #{name} into HTML" do
+ expect(render(data[:file], data[:input], context)).to eql data[:output]
+ end
+ end
+ end
+
+ def render(*args)
+ described_class.render(*args)
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 92e3624a8d8..9a8096208db 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -163,7 +163,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list issue notes when access is restricted" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
results = described_class.new(user, project, note.note)
@@ -172,7 +172,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list merge_request notes when access is restricted" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
results = described_class.new(user, project, note.note)
diff --git a/spec/lib/gitlab/project_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb
new file mode 100644
index 00000000000..e2d6b1b9ab7
--- /dev/null
+++ b/spec/lib/gitlab/project_transfer_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::ProjectTransfer, lib: true do
+ before do
+ @root_dir = File.join(Rails.root, "public", "uploads")
+ @project_transfer = Gitlab::ProjectTransfer.new
+ allow(@project_transfer).to receive(:root_dir).and_return(@root_dir)
+
+ @project_path_was = "test_project_was"
+ @project_path = "test_project"
+ @namespace_path_was = "test_namespace_was"
+ @namespace_path = "test_namespace"
+ end
+
+ after do
+ FileUtils.rm_rf([
+ File.join(@root_dir, @namespace_path),
+ File.join(@root_dir, @namespace_path_was)
+ ])
+ end
+
+ describe '#move_project' do
+ it "moves project upload to another namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_project' do
+ it "renames project" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
+ @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_namespace' do
+ it "renames namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @project_transfer.rename_namespace(@namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ 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/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 6b689c41ef6..84cfd934fa0 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -42,14 +42,85 @@ describe Gitlab::ReferenceExtractor, lib: true do
> @offteam
})
+
expect(subject.users).to match_array([])
end
+ describe 'directly addressed users' do
+ before do
+ @u_foo = create(:user, username: 'foo')
+ @u_foo2 = create(:user, username: 'foo2')
+ @u_foo3 = create(:user, username: 'foo3')
+ @u_foo4 = create(:user, username: 'foo4')
+ @u_foo5 = create(:user, username: 'foo5')
+
+ @u_bar = create(:user, username: 'bar')
+ @u_bar2 = create(:user, username: 'bar2')
+ @u_bar3 = create(:user, username: 'bar3')
+ @u_bar4 = create(:user, username: 'bar4')
+
+ @u_tom = create(:user, username: 'tom')
+ @u_tom2 = create(:user, username: 'tom2')
+ end
+
+ context 'when a user is directly addressed' do
+ it 'accesses the user object which is mentioned in the beginning of the line' do
+ subject.analyze('@foo What do you think? cc: @bar, @tom')
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo])
+ end
+
+ it "doesn't access the user object if it's not mentioned in the beginning of the line" do
+ subject.analyze('What do you think? cc: @bar')
+
+ expect(subject.directly_addressed_users).to be_empty
+ end
+ end
+
+ context 'when multiple users are addressed' do
+ it 'accesses the user objects which are mentioned in the beginning of the line' do
+ subject.analyze('@foo @bar What do you think? cc: @tom')
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo, @u_bar])
+ end
+
+ it "doesn't access the user objects if they are not mentioned in the beginning of the line" do
+ subject.analyze('What do you think? cc: @foo @bar @tom')
+
+ expect(subject.directly_addressed_users).to be_empty
+ end
+ end
+
+ context 'when multiple users are addressed in different paragraphs' do
+ it 'accesses user objects which are mentioned in the beginning of each paragraph' do
+ subject.analyze <<-NOTE.strip_heredoc
+ @foo What do you think? cc: @tom
+
+ - @bar can you please have a look?
+
+ >>>
+ @foo2 what do you think? cc: @bar2
+ >>>
+
+ @foo3 @foo4 thank you!
+
+ > @foo5 well done!
+
+ 1. @bar3 Can you please check? cc: @tom2
+ 2. @bar4 What do you this of this MR?
+ NOTE
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo, @u_foo3, @u_foo4])
+ end
+ end
+ end
+
it 'accesses valid issue objects' do
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.")
+
expect(subject.issues).to match_array([@i0, @i1])
end
@@ -58,6 +129,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict')
subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.")
+
expect(subject.merge_requests).to match_array([@m1, @m0])
end
@@ -67,6 +139,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@l2 = create(:label)
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
+
expect(subject.labels).to match_array([@l0, @l1])
end
@@ -76,6 +149,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@s2 = create(:project_snippet)
subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
+
expect(subject.snippets).to match_array([@s0, @s1])
end
@@ -127,6 +201,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'handles project issue references' do
subject.analyze("this refers issue #{issue.to_reference(project)}")
+
extracted = subject.issues
expect(extracted.size).to eq(1)
expect(extracted).to match_array([issue])
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index c78cd30157e..ba45e2d758c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -2,47 +2,64 @@
require 'spec_helper'
describe Gitlab::Regex, lib: true do
- describe 'project path regex' do
- it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) }
- it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) }
- it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) }
- it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) }
- it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) }
- it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) }
- it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) }
+ describe '.project_path_regex' do
+ subject { described_class.project_path_regex }
+
+ it { is_expected.to match('gitlab-ce') }
+ it { is_expected.to match('gitlab_git') }
+ it { is_expected.to match('_underscore.js') }
+ it { is_expected.to match('100px.com') }
+ it { is_expected.not_to match('?gitlab') }
+ it { is_expected.not_to match('git lab') }
+ it { is_expected.not_to match('gitlab.git') }
end
- describe 'project name regex' do
- it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) }
- it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) }
- it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) }
- it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) }
- it { expect('Český název').to match(Gitlab::Regex.project_name_regex) }
- it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) }
- it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) }
+ describe '.project_name_regex' do
+ subject { described_class.project_name_regex }
+
+ it { is_expected.to match('gitlab-ce') }
+ it { is_expected.to match('GitLab CE') }
+ it { is_expected.to match('100 lines') }
+ it { is_expected.to match('gitlab.git') }
+ it { is_expected.to match('Český název') }
+ it { is_expected.to match('Dash – is this') }
+ it { is_expected.not_to match('?gitlab') }
end
- describe 'file name regex' do
- it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) }
+ describe '.file_name_regex' do
+ subject { described_class.file_name_regex }
+
+ it { is_expected.to match('foo@bar') }
end
- describe 'file path regex' do
- it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
+ describe '.file_path_regex' do
+ subject { described_class.file_path_regex }
+
+ it { is_expected.to match('foo@/bar') }
end
- describe 'environment slug regex' do
- def be_matched
- match(Gitlab::Regex.environment_slug_regex)
- end
+ describe '.environment_slug_regex' do
+ subject { described_class.environment_slug_regex }
+
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo-1') }
+ it { is_expected.not_to match('FOO') }
+ it { is_expected.not_to match('foo/1') }
+ it { is_expected.not_to match('foo.1') }
+ it { is_expected.not_to match('foo*1') }
+ it { is_expected.not_to match('9foo') }
+ it { is_expected.not_to match('foo-') }
+ end
- it { expect('foo').to be_matched }
- it { expect('foo-1').to be_matched }
+ describe 'FULL_NAMESPACE_REGEX_STR' do
+ subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
- it { expect('FOO').not_to be_matched }
- it { expect('foo/1').not_to be_matched }
- it { expect('foo.1').not_to be_matched }
- it { expect('foo*1').not_to be_matched }
- it { expect('9foo').not_to be_matched }
- it { expect('foo-').not_to be_matched }
+ it { is_expected.to match('gitlab.org') }
+ it { is_expected.to match('gitlab.org/gitlab-git') }
+ it { is_expected.not_to match('gitlab.org.') }
+ it { is_expected.not_to match('gitlab.org/') }
+ it { is_expected.not_to match('/gitlab.org') }
+ it { is_expected.not_to match('gitlab.git') }
+ it { is_expected.not_to match('gitlab git') }
end
end
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/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb
new file mode 100644
index 00000000000..2370f56a613
--- /dev/null
+++ b/spec/lib/gitlab/route_map_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::RouteMap, lib: true do
+ describe '#initialize' do
+ context 'when the data is not YAML' do
+ it 'raises an error' do
+ expect { described_class.new('"') }.
+ to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/)
+ end
+ end
+
+ context 'when the data is not a YAML array' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump('foo')) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /an array/)
+ end
+ end
+
+ context 'when an entry is not a hash' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump(['foo'])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /a hash/)
+ end
+ end
+
+ context 'when an entry does not have a source key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /source key/)
+ end
+ end
+
+ context 'when an entry does not have a public key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /public key/)
+ end
+ end
+
+ context 'when an entry source is not a valid regex' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /regular expression/)
+ end
+ end
+
+ context 'when all is good' do
+ it 'returns a route map' do
+ route_map = described_class.new(YAML.dump([{ 'source' => 'index.haml', 'public' => 'index.html' }, { 'source' => '/(.*)\.md/', 'public' => '\1.html' }]))
+
+ expect(route_map.public_path_for_source_path('index.haml')).to eq('index.html')
+ expect(route_map.public_path_for_source_path('foo.md')).to eq('foo.html')
+ end
+ end
+ end
+
+ describe '#public_path_for_source_path' do
+ subject do
+ described_class.new(<<-'MAP'.strip_heredoc)
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+
+ # Blogposts
+ - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+ # HTML files
+ - source: /source/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+ # Other files
+ - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+ MAP
+ end
+
+ it 'returns the public path for a provided source path' do
+ expect(subject.public_path_for_source_path('data/team.yml')).to eq('team/')
+
+ expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/')
+
+ expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html')
+
+ expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png')
+
+ expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil
+ 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/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serialize/ci/variables_spec.rb
deleted file mode 100644
index 7ea74da5252..00000000000
--- a/spec/lib/gitlab/serialize/ci/variables_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Serialize::Ci::Variables do
- subject do
- described_class.load(described_class.dump(object))
- end
-
- let(:object) do
- [{ key: :key, value: 'value', public: true },
- { key: 'wee', value: 1, public: false }]
- end
-
- it 'converts keys into strings' do
- is_expected.to eq([
- { key: 'key', value: 'value', public: true },
- { key: 'wee', value: 1, public: false }])
- end
-end
diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
new file mode 100644
index 00000000000..c4b7fda5dbb
--- /dev/null
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Serializer::Ci::Variables do
+ subject do
+ described_class.load(described_class.dump(object))
+ end
+
+ let(:object) do
+ [{ key: :key, value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }]
+ end
+
+ it 'converts keys into strings' do
+ is_expected.to eq([
+ { key: 'key', value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }
+ ])
+ end
+end
diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb
new file mode 100644
index 00000000000..519eb1b274f
--- /dev/null
+++ b/spec/lib/gitlab/serializer/pagination_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Serializer::Pagination do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:headers) { spy('headers') }
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(params)
+
+ allow(response).to receive(:headers)
+ .and_return(headers)
+ end
+
+ let(:pagination) { described_class.new(request, response) }
+
+ describe '#paginate' do
+ subject { pagination.paginate(resource) }
+
+ let(:resource) { User.all }
+ let(:params) { { page: 1, per_page: 2 } }
+
+ context 'when a multiple resources are present in relation' do
+ before { create_list(:user, 3) }
+
+ it 'correctly paginates the resource' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(headers).to receive(:[]=).with('X-Total', '3')
+ expect(headers).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(headers).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when an invalid resource is about to be paginated' do
+ let(:resource) { create(:user) }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(
+ described_class::InvalidResourceError)
+ end
+ end
+ 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/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb
index 1e4954c4af8..d7f77486b3e 100644
--- a/spec/lib/gitlab/slash_commands/extractor_spec.rb
+++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb
@@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do
let(:original_msg) { "/assign @joe\nworld" }
let(:final_msg) { "world" }
end
+
+ it 'allows slash in command arguments' do
+ msg = "/assign @joe / @jane\nworld"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['assign', '@joe / @jane']]
+ expect(msg).to eq 'world'
+ end
end
context 'in the middle of content' do
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 45cec65a284..9213ced7b19 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -4,16 +4,15 @@ describe Gitlab::Template::IssueTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
- let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ 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 ae51b79be22..77dd3079e22 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -4,16 +4,15 @@ describe Gitlab::Template::MergeRequestTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
- let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ path: 'merge_request_templates'
+ })
end
describe '.all' do
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
deleted file mode 100644
index 7a140518dd2..00000000000
--- a/spec/lib/gitlab/themes_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Themes, lib: true do
- describe '.body_classes' do
- it 'returns a space-separated list of class names' do
- css = described_class.body_classes
-
- expect(css).to include('ui_graphite')
- expect(css).to include(' ui_charcoal ')
- expect(css).to include(' ui_blue')
- end
- end
-
- describe '.by_id' do
- it 'returns a Theme by its ID' do
- expect(described_class.by_id(1).name).to eq 'Graphite'
- expect(described_class.by_id(6).name).to eq 'Blue'
- end
- end
-
- describe '.default' do
- it 'returns the default application theme' do
- allow(described_class).to receive(:default_id).and_return(2)
- expect(described_class.default.id).to eq 2
- end
-
- it 'prevents an infinite loop when configuration default is invalid' do
- default = described_class::APPLICATION_DEFAULT
- themes = described_class::THEMES
-
- config = double(default_theme: 0).as_null_object
- allow(Gitlab).to receive(:config).and_return(config)
- expect(described_class.default.id).to eq default
-
- config = double(default_theme: themes.size + 5).as_null_object
- allow(Gitlab).to receive(:config).and_return(config)
- expect(described_class.default.id).to eq default
- end
- end
-
- describe '.each' do
- it 'passes the block to the THEMES Array' do
- ids = []
- described_class.each { |theme| ids << theme.id }
- expect(ids).not_to be_empty
- end
- end
-end
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/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb
deleted file mode 100644
index 4092f7fb638..00000000000
--- a/spec/lib/gitlab/uploads_transfer_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::UploadsTransfer, lib: true do
- before do
- @root_dir = File.join(Rails.root, "public", "uploads")
- @upload_transfer = Gitlab::UploadsTransfer.new
-
- @project_path_was = "test_project_was"
- @project_path = "test_project"
- @namespace_path_was = "test_namespace_was"
- @namespace_path = "test_namespace"
- end
-
- after do
- FileUtils.rm_rf([
- File.join(@root_dir, @namespace_path),
- File.join(@root_dir, @namespace_path_was)
- ])
- end
-
- describe '#move_project' do
- it "moves project upload to another namespace" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-
- describe '#rename_project' do
- it "renames project" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
- @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-
- describe '#rename_namespace' do
- it "renames namespace" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 2cb74629da8..3fd361de458 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -70,4 +70,12 @@ describe Gitlab::UrlSanitizer, lib: true do
expect(sanitizer.full_url).to eq('user@server:project.git')
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
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/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 7dd4d76d1a3..8e5e8288c49 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do
out = {
subprotocols: ['foo'],
url: 'wss://example.com/terminal.ws',
- headers: { 'Authorization' => ['Token x'] }
+ headers: { 'Authorization' => ['Token x'] },
+ max_session_time: 600
}
out[:ca_pem] = ca_pem if ca_pem
out
@@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do
'Terminal' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
- 'Header' => { 'Authorization' => ['Token x'] }
+ 'Header' => { 'Authorization' => ['Token x'] },
+ 'MaxSessionTime' => 600
}
}
out['Terminal']['CAPem'] = ca_pem if ca_pem
@@ -197,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/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 2f4a33a1868..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) }
@@ -247,7 +253,7 @@ describe Ability, lib: true do
end
describe '.project_disabled_features_rules' do
- let(:project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED) }
+ let(:project) { create(:empty_project, :wiki_disabled) }
subject { described_class.allowed(project.owner, 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 e20b394c525..fd6ea2d6722 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Ci::Build, :models do
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:test_trace) { 'This is a test' }
@@ -19,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? }
@@ -161,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
@@ -174,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') }
@@ -207,14 +224,16 @@ describe Ci::Build, :models do
end
it 'expects to have retried builds instead the original ones' do
- retried_rspec = Ci::Build.retry(rspec_test)
- expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
+ project.add_developer(user)
+
+ retried_rspec = Ci::Build.retry(rspec_test, user)
+
+ expect(staging.depends_on_builds.map(&:id))
+ .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
end
end
describe '#detailed_status' do
- let(:user) { create(:user) }
-
it 'returns a detailed status' do
expect(build.detailed_status(user))
.to be_a Gitlab::Ci::Status::Build::Cancelable
@@ -326,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') }
@@ -484,11 +503,11 @@ describe Ci::Build, :models do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
subject { build.erased? }
- context 'build has not been erased' do
+ context 'job has not been erased' do
it { is_expected.to be_falsey }
end
- context 'build has been erased' do
+ context 'job has been erased' do
before do
build.erase
end
@@ -592,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
@@ -687,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' }
@@ -708,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
@@ -813,12 +876,16 @@ describe Ci::Build, :models do
subject { build.other_actions }
+ before do
+ project.add_developer(user)
+ end
+
it 'returns other actions' do
is_expected.to contain_exactly(other_build)
end
context 'when build is retried' do
- let!(:new_build) { Ci::Build.retry(build) }
+ let!(:new_build) { Ci::Build.retry(build, user) }
it 'does not return any of them' do
is_expected.not_to include(build, new_build)
@@ -826,7 +893,7 @@ describe Ci::Build, :models do
end
context 'when other build is retried' do
- let!(:retried_build) { Ci::Build.retry(other_build) }
+ let!(:retried_build) { Ci::Build.retry(other_build, user) }
it 'returns a retried build' do
is_expected.to contain_exactly(retried_build)
@@ -848,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
@@ -857,21 +924,29 @@ describe Ci::Build, :models do
describe '#play' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
- subject { build.play }
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when build is manual' do
+ it 'enqueues a build' do
+ new_build = build.play(user)
- it 'enqueues a build' do
- is_expected.to be_pending
- is_expected.to eq(build)
+ expect(new_build).to be_pending
+ expect(new_build).to eq(build)
+ end
end
- context 'for successful build' do
+ context 'when build is passed' do
before do
build.update(status: 'success')
end
it 'creates a new build' do
- is_expected.to be_pending
- is_expected.not_to eq(build)
+ new_build = build.play(user)
+
+ expect(new_build).to be_pending
+ expect(new_build).not_to eq(build)
end
end
end
@@ -1211,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
@@ -1242,16 +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) { create(:user, username: 'starter') }
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
@@ -1283,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) }
@@ -1291,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
@@ -1320,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
@@ -1344,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
@@ -1356,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
@@ -1374,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
@@ -1408,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 426be74cd02..9962c987110 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3,8 +3,12 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
include EmailHelpers
- let(:project) { FactoryGirl.create :empty_project }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, status: :created, project: project)
+ end
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
@@ -20,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
@@ -164,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
@@ -179,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
@@ -195,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
@@ -503,7 +533,9 @@ describe Ci::Pipeline, models: true do
end
describe '#status' do
- let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
+ let(:build) do
+ create(:ci_build, :created, pipeline: pipeline, name: 'test')
+ end
subject { pipeline.reload.status }
@@ -545,13 +577,21 @@ describe Ci::Pipeline, models: true do
build.cancel
end
- it { is_expected.to eq('canceled') }
+ context 'when build is pending' do
+ let(:build) do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ it { is_expected.to eq('canceled') }
+ end
end
context 'on failure and build retry' do
before do
build.drop
- Ci::Build.retry(build)
+ project.add_developer(user)
+
+ Ci::Build.retry(build, user)
end
# We are changing a state: created > failed > running
@@ -563,8 +603,6 @@ describe Ci::Pipeline, models: true do
end
describe '#detailed_status' do
- let(:user) { create(:user) }
-
subject { pipeline.detailed_status(user) }
context 'when pipeline is created' do
@@ -623,6 +661,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) }
@@ -720,7 +766,7 @@ describe Ci::Pipeline, models: true do
describe '#cancel_running' do
let(:latest_status) { pipeline.statuses.pluck(:status) }
- context 'when there is a running external job and created build' do
+ context 'when there is a running external job and a regular job' do
before do
create(:ci_build, :running, pipeline: pipeline)
create(:generic_commit_status, :running, pipeline: pipeline)
@@ -733,7 +779,7 @@ describe Ci::Pipeline, models: true do
end
end
- context 'when builds are in different stages' do
+ context 'when jobs are in different stages' do
before do
create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
create(:ci_build, :running, stage_idx: 1, pipeline: pipeline)
@@ -745,17 +791,34 @@ describe Ci::Pipeline, models: true do
expect(latest_status).to contain_exactly('canceled', 'canceled')
end
end
+
+ context 'when there are created builds present in the pipeline' do
+ before do
+ create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
+ create(:ci_build, :created, stage_idx: 1, pipeline: pipeline)
+
+ pipeline.cancel_running
+ end
+
+ it 'cancels created builds' do
+ expect(latest_status).to eq %w(canceled canceled)
+ end
+ end
end
describe '#retry_failed' do
let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
+ before do
+ project.add_developer(user)
+ end
+
context 'when there is a failed build and failed external status' do
before do
create(:ci_build, :failed, name: 'build', pipeline: pipeline)
create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries only build' do
@@ -768,11 +831,11 @@ describe Ci::Pipeline, models: true do
create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries both builds' do
- expect(latest_status).to contain_exactly('pending', 'pending')
+ expect(latest_status).to contain_exactly('pending', 'created')
end
end
@@ -781,11 +844,11 @@ describe Ci::Pipeline, models: true do
create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries both builds' do
- expect(latest_status).to contain_exactly('pending', 'pending')
+ expect(latest_status).to contain_exactly('pending', 'created')
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 3f32248e52b..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
@@ -290,7 +290,7 @@ describe Ci::Runner, models: true do
let!(:last_update) { runner.ensure_runner_queue_value }
before do
- runner.update(description: 'new runner')
+ Ci::UpdateRunnerService.new(runner).update(description: 'new runner')
end
it 'sets a new last_update value' do
@@ -318,6 +318,25 @@ describe Ci::Runner, models: true do
end
end
+ describe '#destroy' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when there is a tick in the queue' do
+ let!(:queue_key) { runner.send(:runner_queue_key) }
+
+ before do
+ runner.tick_runner_queue
+ runner.destroy
+ end
+
+ it 'cleans up the queue' do
+ Gitlab::Redis.with do |redis|
+ expect(redis.get(queue_key)).to be_nil
+ end
+ end
+ end
+ end
+
describe '.assignable_for' do
let(:runner) { create(:ci_runner) }
let(:project) { create(:empty_project) }
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/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index bf4394f7d5b..ea5e4e21039 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe CommitStatus, models: true do
+describe CommitStatus, :models do
let(:project) { create(:project, :repository) }
let(:pipeline) do
@@ -127,7 +127,7 @@ describe CommitStatus, models: true do
end
describe '.latest' do
- subject { CommitStatus.latest.order(:id) }
+ subject { described_class.latest.order(:id) }
let(:statuses) do
[create_status(name: 'aa', ref: 'bb', status: 'running'),
@@ -143,7 +143,7 @@ describe CommitStatus, models: true do
end
describe '.running_or_pending' do
- subject { CommitStatus.running_or_pending.order(:id) }
+ subject { described_class.running_or_pending.order(:id) }
let(:statuses) do
[create_status(name: 'aa', ref: 'bb', status: 'running'),
@@ -158,8 +158,22 @@ describe CommitStatus, models: true do
end
end
+ describe '.after_stage' do
+ subject { described_class.after_stage(0) }
+
+ let(:statuses) do
+ [create_status(name: 'aa', stage_idx: 0),
+ create_status(name: 'cc', stage_idx: 1),
+ create_status(name: 'aa', stage_idx: 2)]
+ end
+
+ it 'returns statuses from second and third stage' do
+ is_expected.to eq(statuses.values_at(1, 2))
+ end
+ end
+
describe '.exclude_ignored' do
- subject { CommitStatus.exclude_ignored.order(:id) }
+ subject { described_class.exclude_ignored.order(:id) }
let(:statuses) do
[create_status(when: 'manual', status: 'skipped'),
@@ -171,11 +185,32 @@ describe CommitStatus, models: true 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..f134da441c2 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -109,6 +109,24 @@ describe HasStatus do
it { is_expected.to eq 'running' }
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 +236,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 +255,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/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
new file mode 100644
index 00000000000..69906382545
--- /dev/null
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -0,0 +1,104 @@
+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 '#min_relative_position' do
+ it 'returns maximum position' do
+ expect(issue.min_relative_position).to eq issue.relative_position
+ 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 minimum position if there is no issue above' do
+ expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION
+ 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 next position if there is no issue below' do
+ expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION
+ 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 '#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
+ end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 30443534cca..677e60e1282 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -14,12 +14,14 @@ describe Group, 'Routable' do
describe 'Callbacks' do
it 'creates route record on create' do
expect(group.route.path).to eq(group.path)
+ expect(group.route.name).to eq(group.name)
end
it 'updates route record on path change' do
- group.update_attributes(path: 'wow')
+ group.update_attributes(path: 'wow', name: 'much')
expect(group.route.path).to eq('wow')
+ expect(group.route.name).to eq('much')
end
it 'ensure route path uniqueness across different objects' do
@@ -78,4 +80,34 @@ describe Group, 'Routable' do
it { is_expected.to eq([nested_group]) }
end
+
+ describe '#full_path' do
+ let(:group) { create(:group) }
+ 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.full_path}/#{nested_group.path}") }
+ end
+
+ describe '#full_name' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_name).to eq(group.name) }
+ it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+ end
+end
+
+describe Project, 'Routable' do
+ describe '#full_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
+ end
+
+ describe '#full_name' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" }
+ end
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index 32935bc0b09..fd3b8307571 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -14,14 +14,16 @@ describe Issue, 'Spammable' do
end
describe 'InstanceMethods' do
+ let(:issue) { build(:issue, spam: true) }
+
it 'should be invalid if spam' do
- issue = build(:issue, spam: true)
expect(issue.valid?).to be_falsey
end
describe '#check_for_spam?' do
it 'returns true for public project' do
issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+
expect(issue.check_for_spam?).to eq(true)
end
@@ -29,5 +31,20 @@ describe Issue, 'Spammable' do
expect(issue.check_for_spam?).to eq(false)
end
end
+
+ describe '#submittable_as_spam_by?' do
+ let(:admin) { build(:admin) }
+ let(:user) { build(:user) }
+
+ before do
+ allow(issue).to receive(:submittable_as_spam?).and_return(true)
+ end
+
+ it 'tests if the user can submit spam' do
+ expect(issue.submittable_as_spam_by?(admin)).to be(true)
+ expect(issue.submittable_as_spam_by?(user)).to be(false)
+ expect(issue.submittable_as_spam_by?(nil)).to be_nil
+ end
+ end
end
end
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 2cbee741fb0..e6a826a9418 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -21,7 +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(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ sha = context.project.repository.create_file(
+ context.user,
+ context.random_git_name,
+ 'content',
+ message: 'commit message',
+ 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 104e65335dd..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",
- "commit message",
- 'master',
- false)
+ 'content',
+ message: 'commit message',
+ branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index fc4435a2f64..080ff2f3f43 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -77,8 +77,8 @@ describe Deployment, models: true do
end
end
- describe '#stoppable?' do
- subject { deployment.stoppable? }
+ describe '#stop_action?' do
+ subject { deployment.stop_action? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index eba392044bf..b4305e92812 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -7,8 +7,6 @@ describe Environment, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
-
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -22,6 +20,20 @@ describe Environment, models: true do
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
+ describe '.order_by_last_deployed_at' do
+ let(:project) { create(:project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:environment3) { create(:environment, project: project) }
+ let!(:deployment1) { create(:deployment, environment: environment1) }
+ let!(:deployment2) { create(:deployment, environment: environment2) }
+ let!(:deployment3) { create(:deployment, environment: environment1) }
+
+ it 'returns the environments in order of having been last deployed' do
+ expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1])
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -64,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,
@@ -112,8 +125,8 @@ describe Environment, models: true do
end
end
- describe '#stoppable?' do
- subject { environment.stoppable? }
+ describe '#stop_action?' do
+ subject { environment.stop_action? }
context 'when no other actions' do
it { is_expected.to be_falsey }
@@ -142,17 +155,39 @@ describe Environment, models: true do
end
end
- describe '#stop!' do
- let(:user) { create(:user) }
+ describe '#stop_with_action!' do
+ let(:user) { create(:admin) }
- subject { environment.stop!(user) }
+ subject { environment.stop_with_action!(user) }
before do
- expect(environment).to receive(:stoppable?).and_call_original
+ expect(environment).to receive(:available?).and_call_original
end
context 'when no other actions' do
- it { is_expected.to be_nil }
+ context 'environment is available' do
+ before do
+ environment.update(state: :available)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
+
+ context 'environment is already stopped' do
+ before do
+ environment.update(state: :stopped)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
end
context 'when matching action is defined' do
@@ -236,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
@@ -246,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
@@ -277,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,
@@ -301,4 +405,33 @@ describe Environment, models: true do
end
end
end
+
+ describe '#external_url_for' do
+ let(:source_path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ environment.external_url = 'http://example.com'
+ end
+
+ context 'when the public path is not known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(environment.external_url_for(source_path, sha)).to be_nil
+ end
+ end
+
+ context 'when the public path is known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html')
+ end
+
+ it 'returns the full external URL' do
+ expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html')
+ end
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 349474bb656..8c90a538f57 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -19,7 +19,7 @@ describe Event, models: true do
let(:project) { create(:empty_project) }
it 'calls the reset_project_activity method' do
- expect_any_instance_of(Event).to receive(:reset_project_activity)
+ expect_any_instance_of(described_class).to receive(:reset_project_activity)
create_event(project, project.owner)
end
@@ -43,33 +43,33 @@ describe Event, models: true do
describe '#membership_changed?' do
context "created" do
- subject { build(:event, action: Event::CREATED).membership_changed? }
+ subject { build(:event, :created).membership_changed? }
it { is_expected.to be_falsey }
end
context "updated" do
- subject { build(:event, action: Event::UPDATED).membership_changed? }
+ subject { build(:event, :updated).membership_changed? }
it { is_expected.to be_falsey }
end
context "expired" do
- subject { build(:event, action: Event::EXPIRED).membership_changed? }
+ subject { build(:event, :expired).membership_changed? }
it { is_expected.to be_truthy }
end
context "left" do
- subject { build(:event, action: Event::LEFT).membership_changed? }
+ subject { build(:event, :left).membership_changed? }
it { is_expected.to be_truthy }
end
context "joined" do
- subject { build(:event, action: Event::JOINED).membership_changed? }
+ subject { build(:event, :joined).membership_changed? }
it { is_expected.to be_truthy }
end
end
describe '#note?' do
- subject { Event.new(project: target.project, target: target) }
+ subject { described_class.new(project: target.project, target: target) }
context 'issue note event' do
let(:target) { create(:note_on_issue) }
@@ -97,7 +97,7 @@ describe Event, models: true do
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
- let(:event) { Event.new(project: project, target: target, author_id: author.id) }
+ let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
before do
project.team << [member, :developer]
@@ -221,13 +221,13 @@ describe Event, models: true do
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
- subject { Event.limit_recent }
+ subject { described_class.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
- subject { Event.limit_recent(1) }
+ subject { described_class.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
@@ -294,9 +294,9 @@ describe Event, models: true do
}
}
- Event.create({
+ described_class.create({
project: project,
- action: Event::PUSHED,
+ action: described_class::PUSHED,
data: data,
author_id: user.id
}.merge!(attrs))
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 9ca50555191..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) }
@@ -300,4 +302,17 @@ describe Group, models: true do
expect(group.members_with_parents).to include(master)
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ master = create(:user)
+ developer = create(:user)
+
+ group.add_user(master, GroupMember::MASTER)
+ group.add_user(developer, GroupMember::DEVELOPER)
+
+ expect(group.user_ids_for_project_authorizations).
+ to include(master.id, developer.id)
+ end
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index 582b54c0712..c60bd7af958 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -37,8 +37,6 @@ describe Guest, lib: true do
context 'when repository is enabled' do
it 'allows to pull the repo' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
-
expect(Guest.can?(:download_code, public_project)).to eq(true)
end
end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index 9e1a52011c3..e6ca4853873 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -19,13 +19,6 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
- context 'when list_type is set to backlog' do
- subject { described_class.new(list_type: :backlog) }
-
- it { is_expected.not_to validate_presence_of(:label) }
- it { is_expected.not_to validate_presence_of(:position) }
- end
-
context 'when list_type is set to done' do
subject { described_class.new(list_type: :done) }
@@ -41,12 +34,6 @@ describe List do
expect(subject.destroy).to be_truthy
end
- it 'can not be destroyed when list_type is set to backlog' do
- subject = create(:backlog_list)
-
- expect(subject.destroy).to be_falsey
- end
-
it 'can not be destroyed when when list_type is set to done' do
subject = create(:done_list)
@@ -55,19 +42,13 @@ describe List do
end
describe '#destroyable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_destroyable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_destroyable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_destroyable
@@ -75,19 +56,13 @@ describe List do
end
describe '#movable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_movable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_movable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_movable
@@ -102,12 +77,6 @@ describe List do
expect(subject.title).to eq 'Development'
end
- it 'returns Backlog when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject.title).to eq 'Backlog'
- end
-
it 'returns Done when list_type is set to done' do
subject.list_type = :done
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 16e2144d6a1..c720cc9f2c2 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -129,6 +129,14 @@ describe Member, models: true do
it { expect(described_class.request).not_to include @accepted_request_member }
end
+ describe '.non_request' do
+ it { expect(described_class.non_request).to include @master }
+ it { expect(described_class.non_request).to include @invited_member }
+ it { expect(described_class.non_request).to include @accepted_invite_member }
+ it { expect(described_class.non_request).not_to include @requested_member }
+ it { expect(described_class.non_request).to include @accepted_request_member }
+ end
+
describe '.developers' do
subject { described_class.developers.to_a }
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 32ed1e96749..fcaf4c71182 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
@@ -97,7 +97,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
@@ -106,7 +106,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
@@ -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) }
@@ -300,7 +344,24 @@ describe MergeRequest, models: true do
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
+ 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
@@ -464,17 +525,17 @@ 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
@@ -768,7 +829,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 +850,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 +944,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 +987,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
@@ -1005,10 +1060,16 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
+ describe "#environments_for" do
let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ merge_request.source_project.add_master(user)
+ merge_request.target_project.add_master(user)
+ end
+
context 'with multiple environments' do
let(:environments) { create_list(:environment, 3, project: project) }
@@ -1018,7 +1079,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(environments.first)
+ expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
end
end
@@ -1042,7 +1103,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
end
context 'with environments on target project' do
@@ -1053,7 +1114,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
end
end
end
@@ -1064,7 +1125,7 @@ describe MergeRequest, models: true do
end
it 'returns an empty array' do
- expect(merge_request.environments).to be_empty
+ expect(merge_request.environments_for(user)).to be_empty
end
end
end
@@ -1531,7 +1592,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 4e96f19eb6f..757f3921450 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -3,21 +3,46 @@ require 'spec_helper'
describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
- it { is_expected.to have_many :projects }
- it { is_expected.to have_many :project_statistics }
- it { is_expected.to belong_to :parent }
- it { is_expected.to have_many :children }
+ describe 'associations' do
+ it { is_expected.to have_many :projects }
+ it { is_expected.to have_many :project_statistics }
+ it { is_expected.to belong_to :parent }
+ it { is_expected.to have_many :children }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(255) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_length_of(:path).is_at_most(255) }
+ it { is_expected.to validate_presence_of(:owner) }
+
+ it 'does not allow too deep nesting' do
+ ancestors = (1..21).to_a
+ nested = build(:namespace, parent: namespace)
+
+ allow(nested).to receive(:ancestors).and_return(ancestors)
+
+ expect(nested).not_to be_valid
+ expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting')
+ end
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
- it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ describe 'reserved path validation' do
+ context 'nested group' do
+ let(:group) { build(:group, :nested, path: 'tree') }
- it { is_expected.to validate_length_of(:description).is_at_most(255) }
+ it { expect(group).not_to be_valid }
+ end
- it { is_expected.to validate_presence_of(:path) }
- it { is_expected.to validate_length_of(:path).is_at_most(255) }
+ context 'top-level group' do
+ let(:group) { build(:group, path: 'tree') }
- it { is_expected.to validate_presence_of(:owner) }
+ it { expect(group).to be_valid }
+ end
+ end
+ end
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
@@ -25,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
@@ -140,7 +165,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
@@ -175,22 +200,6 @@ describe Namespace, models: true do
end
end
- describe '#full_path' do
- let(:group) { create(:group) }
- 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}") }
- end
-
- describe '#full_name' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
-
- it { expect(group.full_name).to eq(group.name) }
- it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
- end
-
describe '#ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
@@ -218,4 +227,11 @@ describe Namespace, models: true do
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ expect(namespace.user_ids_for_project_authorizations).
+ to eq([namespace.owner_id])
+ end
+ end
end
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/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
new file mode 100644
index 00000000000..e6a4583a8fb
--- /dev/null
+++ b/spec/models/pages_domain_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe PagesDomain, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe :validate_domain do
+ subject { build(:pages_domain, domain: domain) }
+
+ context 'is unique' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to validate_uniqueness_of(:domain) }
+ end
+
+ context 'valid domain' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'valid hexadecimal-looking domain' do
+ let(:domain) { '0x12345.com'}
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'no domain' do
+ let(:domain) { nil }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'invalid domain' do
+ let(:domain) { '0123123' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'domain from .example.com' do
+ let(:domain) { 'my.domain.com' }
+
+ before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe 'validate certificate' do
+ subject { domain }
+
+ context 'when only certificate is specified' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when only key is specified' do
+ let(:domain) { build(:pages_domain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'for not matching key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe :url do
+ subject { domain.url }
+
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain) }
+
+ it { is_expected.to eq('http://my.domain.com') }
+ end
+
+ context 'with a certificate' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to eq('https://my.domain.com') }
+ end
+ end
+
+ describe :has_matching_key? do
+ subject { domain.has_matching_key? }
+
+ context 'for matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for invalid key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe :has_intermediates? do
+ subject { domain.has_intermediates? }
+
+ context 'for self signed' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for missing certificate chain' do
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for trusted certificate chain' do
+ # We only validate that we can to rebuild the trust chain, for certificates
+ # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store.
+ # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine
+
+ let(:domain) { build(:pages_domain, :with_trusted_chain) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :expired? do
+ subject { domain.expired? }
+
+ context 'for valid' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for expired' do
+ let(:domain) { build(:pages_domain, :with_expired_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :subject do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.subject }
+
+ it { is_expected.to eq('/CN=test-certificate') }
+ end
+
+ describe :certificate_text do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.certificate_text }
+
+ # We test only existence of output, since the output is long
+ it { is_expected.not_to be_empty }
+ 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_feature_spec.rb b/spec/models/project_feature_spec.rb
index 8589f1eb712..09a4448d387 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -57,7 +57,6 @@ describe ProjectFeature do
context 'when feature is enabled for everyone' do
it "returns true" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
@@ -104,7 +103,6 @@ describe ProjectFeature do
it "returns true when feature is enabled for everyone" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.public_send("#{feature}_enabled?")).to eq(true)
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/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
index 50ad5013df9..3bd7ec18ae0 100644
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do
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(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
@@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do
context 'build failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
@@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do
end
end
- def build_message(status_text = status)
+ 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 hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " 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/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/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 2f6b159d76e..4bca0229e7a 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -135,7 +135,7 @@ describe JiraService, models: true do
url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
- status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } }
+ status: { resolved: true }
}
)
).once
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 4f3cd14e941..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
@@ -181,11 +189,23 @@ describe KubernetesService, models: true, caching: true do
let(:pod) { kube_pod(app: environment.slug) }
let(:terminals) { kube_terminals(service, pod) }
- it 'returns terminals' do
- stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ])
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
+ )
+ end
+ it 'returns terminals' do
is_expected.to eq(terminals + terminals)
end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
end
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 48b085781e7..e120e21af06 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -60,6 +60,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:pages_domains) }
it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
@@ -70,6 +71,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
@@ -177,7 +179,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
@@ -274,13 +276,6 @@ describe Project, models: true do
it { is_expected.to delegate_method(:add_master).to(:team) }
end
- describe '#name_with_namespace' do
- let(:project) { build_stubbed(:empty_project) }
-
- it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
- it { expect(project.human_name).to eq project.name_with_namespace }
- end
-
describe '#to_reference' do
let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
@@ -386,7 +381,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
@@ -408,7 +403,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
@@ -632,7 +627,7 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+ let(:no_wiki_project) { create(:empty_project, :wiki_disabled, has_external_wiki: false) }
let(:wiki_enabled_project) { create(:empty_project) }
let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) }
@@ -809,7 +804,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}" }
@@ -952,8 +947,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
@@ -1067,6 +1062,22 @@ describe Project, models: true do
end
end
+ describe '#pages_deployed?' do
+ let(:project) { create :empty_project }
+
+ subject { project.pages_deployed? }
+
+ context 'if public folder does exist' do
+ before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "if public folder doesn't exist" do
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '.search' do
let(:project) { create(:empty_project, description: 'kitten mittens') }
@@ -1138,16 +1149,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).
@@ -1156,7 +1165,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)
@@ -1528,7 +1537,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
@@ -1699,149 +1708,212 @@ describe Project, models: true do
end
end
- describe '#environments_for' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
+ describe '#deployment_variables' do
+ context 'when project has no deployment service' do
+ let(:project) { create(:empty_project) }
- context 'tagged deployment' do
- before do
- create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ it 'returns an empty array' do
+ expect(project.deployment_variables).to eq []
end
+ end
- it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', commit: project.commit, with_tags: true))
- .to contain_exactly(environment)
- end
+ context 'when project has a deployment service' do
+ let(:project) { create(:kubernetes_project) }
- it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to be_empty
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
+ )
end
+ end
+ end
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
- end
+ describe '#update_project_statistics' do
+ let(:project) { create(:empty_project) }
+
+ it "is called after creation" do
+ expect(project.statistics).to be_a ProjectStatistics
+ expect(project.statistics).to be_persisted
end
- context 'branch deployment' do
- before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
- end
+ it "copies the namespace_id" do
+ expect(project.statistics.namespace_id).to eq project.namespace_id
+ end
- it 'returns environment when ref is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to contain_exactly(environment)
- end
+ it "updates the namespace_id when changed" do
+ namespace = create(:namespace)
+ project.update(namespace: namespace)
+
+ expect(project.statistics.namespace_id).to eq namespace.id
+ end
+ end
- it 'does not environment when ref is different' do
- expect(project.environments_for('feature', commit: project.commit))
- .to be_empty
+ describe 'inside_path' do
+ let!(:project1) { create(:empty_project) }
+ let!(:project2) { create(:empty_project) }
+ let!(:path) { project1.namespace.full_path }
+
+ it { expect(Project.inside_path(path)).to eq([project1]) }
+ end
+
+ describe '#route_map_for' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ 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
+ context 'when the route map is valid' do
+ it 'returns a route map' do
+ map = project.route_map_for(project.commit.sha)
+ expect(map).to be_a_kind_of(Gitlab::RouteMap)
+ end
end
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
+ context 'when the route map is invalid' do
+ let(:route_map) { 'INVALID' }
+
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.sha)).to be_nil
+ end
end
+ end
- it 'returns environment when commit constraint is not set' do
- expect(project.environments_for('master'))
- .to contain_exactly(environment)
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.parent.sha)).to be_nil
end
end
end
- describe '#environments_recently_updated_on_branch' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
+ describe '#public_path_for_source_path' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ Gitlab::RouteMap.new(<<-MAP.strip_heredoc)
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+ let(:sha) { project.commit.id }
- context 'when last deployment to environment is the most recent one' do
+ context 'when there is a route map' do
before do
- create(:deployment, environment: environment, ref: 'feature')
+ allow(project).to receive(:route_map_for).with(sha).and_return(route_map)
end
- it 'finds recently updated environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment)
+ context 'when the source path is mapped' do
+ it 'returns the public path' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html')
+ end
+ end
+
+ context 'when the source path is not mapped' do
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('file.html', sha)).to be_nil
+ end
end
end
- context 'when last deployment to environment is not the most recent' do
+ context 'when there is no route map' do
before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: environment, ref: 'master')
+ allow(project).to receive(:route_map_for).with(sha).and_return(nil)
end
- it 'does not find environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to be_empty
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil
end
end
+ end
- context 'when there are two environments that deploy to the same branch' do
- let(:second_environment) { create(:environment, project: project) }
+ describe '#parent' do
+ let(:project) { create(:empty_project) }
- before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: second_environment, ref: 'feature')
- end
+ it { expect(project.parent).to eq(project.namespace) }
+ end
- it 'finds both environments' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment, second_environment)
- end
- end
+ describe '#parent_changed?' do
+ let(:project) { create(:empty_project) }
+
+ before { project.namespace_id = 7 }
+
+ it { expect(project.parent_changed?).to be_truthy }
end
- describe '#deployment_variables' do
- context 'when project has no deployment service' do
- let(:project) { create(:empty_project) }
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
- it 'returns an empty array' do
- expect(project.deployment_variables).to eq []
- end
+ describe '#pages_url' do
+ let(:group) { create :group, name: 'Group' }
+ let(:nested_group) { create :group, parent: group }
+ let(:domain) { 'Example.com' }
+
+ subject { project.pages_url }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
end
- context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ context 'top-level group' do
+ let(:project) { create :empty_project, namespace: group, name: project_name }
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
+
+ it { is_expected.to eq("http://group.example.com") }
end
- end
- end
- describe '#update_project_statistics' do
- let(:project) { create(:empty_project) }
+ context 'project page' do
+ let(:project_name) { 'Project' }
- it "is called after creation" do
- expect(project.statistics).to be_a ProjectStatistics
- expect(project.statistics).to be_persisted
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
end
- it "copies the namespace_id" do
- expect(project.statistics.namespace_id).to eq project.namespace_id
- end
+ context 'nested group' do
+ let(:project) { create :empty_project, namespace: nested_group, name: project_name }
+ let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
- it "updates the namespace_id when changed" do
- namespace = create(:namespace)
- project.update(namespace: namespace)
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
- expect(project.statistics.namespace_id).to eq namespace.id
+ it { is_expected.to eq(expected_url) }
+ end
+
+ context 'project page' do
+ let(:project_name) { 'Project' }
+
+ it { is_expected.to eq(expected_url) }
+ end
end
end
- describe 'inside_path' do
- let!(:project1) { create(:empty_project) }
- let!(:project2) { create(:empty_project) }
- let!(:path) { project1.namespace.path }
+ describe '#http_url_to_repo' do
+ let(:project) { create :empty_project }
- it { expect(Project.inside_path(path)).to eq([project1]) }
- end
+ context 'when no user is given' do
+ it 'returns the url to the repo without a username' do
+ url = project.http_url_to_repo
- def enable_lfs
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ expect(url).to eq(project.http_url_to_repo)
+ expect(url).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 match(%r{https?:\/\/#{user.username}@})
+ end
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 829b69093c9..274e4f00a0a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -15,7 +15,12 @@ describe Repository, models: true do
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
+
repository.commit(merge_commit_id)
end
@@ -286,20 +291,42 @@ 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', 'Create newdir', 'master')
+ repository.create_dir(user, 'newdir',
+ message: 'Create newdir', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
newdir = repository.tree('master', 'newdir')
expect(newdir.path).to eq('newdir')
end
+ context "when committing to another project" do
+ let(:forked_project) { create(:project) }
+
+ it "creates a fork and commit to the forked project" do
+ expect do
+ 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)
+
+ expect(repository.branch_exists?('patch')).to be_truthy
+ expect(forked_project.repository.branch_exists?('patch')).to be_falsy
+
+ newdir = repository.tree('patch', 'newdir')
+ expect(newdir.path).to eq('newdir')
+ end
+ end
+
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", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+ repository.create_dir(user, 'newdir',
+ message: 'Add newdir',
+ branch_name: 'master',
+ author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -310,24 +337,37 @@ 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!',
- 'Updates file content',
- 'master', 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.create_file(user, 'hello.txt', "Hello,\r\nWorld",
+ message: 'Add hello world',
+ branch_name: 'master')
+
+ blob = repository.blob_at('master', 'hello.txt')
+
+ expect(blob.data).to eq("Hello,\nWorld")
+ end
+
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!', 'Add README',
- 'master', true, author_email: author_email, author_name: author_name)
+ repository.create_file(user, 'NEWREADME', 'README!',
+ message: 'Add README',
+ branch_name: 'master',
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -339,10 +379,22 @@ 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!',
- branch: 'master',
+ branch_name: 'master',
previous_path: 'LICENSE',
message: 'Changes filename')
end.to change { repository.commits('master').count }.by(1)
@@ -355,15 +407,13 @@ 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!', 'Add README', 'master', true)
-
expect do
- repository.update_file(user, 'README', "Updated README!",
- branch: 'master',
- previous_path: 'README',
- message: 'Update README',
- author_email: author_email,
- author_name: author_name)
+ repository.update_file(user, 'README', 'Updated README!',
+ branch_name: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -374,12 +424,11 @@ 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!', 'Add README', 'master', true)
-
expect do
- repository.remove_file(user, "README", "Remove README", 'master')
+ repository.delete_file(user, 'README',
+ message: 'Remove README', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
expect(repository.blob_at('master', 'README')).to be_nil
@@ -387,10 +436,10 @@ 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!', 'Add README', 'master', true)
-
expect do
- repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+ 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)
last_commit = repository.commit
@@ -538,11 +587,14 @@ describe Repository, models: true do
describe "#license_blob", caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ 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(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.create_file(
+ user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
allow(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
@@ -551,21 +603,27 @@ describe Repository, models: true do
end
it 'looks in the root_ref only' do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
- repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'markdown')
+ repository.create_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ 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!', 'Add LICENSE', 'master', 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, Licensee::License.new('mit').content, "Add #{filename}", 'master', false)
+ repository.create_file(user, filename,
+ Licensee::License.new('mit').content,
+ message: "Add #{filename}", branch_name: 'master')
expect(repository.license_blob.name).to eq(filename)
end
@@ -574,7 +632,8 @@ describe Repository, models: true do
describe '#license_key', caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
@@ -588,13 +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!', 'Add LICENSE', 'master', 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', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.create_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq('mit')
end
@@ -707,7 +769,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.rm_branch(user, 'new_feature')
+ repository.rm_branch(user, 'feature')
end.to raise_error(GitHooksService::PreReceiveError)
end
@@ -728,36 +790,51 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
before do
- expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
- and_yield.and_return(true)
+ service = GitHooksService.new
+ expect(GitHooksService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute).
+ with(
+ user,
+ repository.path_to_repo,
+ old_rev,
+ new_rev,
+ 'refs/heads/feature').
+ and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
- expect(repository).to receive(:update_autocrlf_option)
+ service = GitOperationService.new(user, repository)
+
+ expect(service).to receive(:update_autocrlf_option)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ service.with_branch('feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
+
expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
end
end
end
context 'when the update adds more than one commit' do
- it 'runs without errors' do
- old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+ let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' }
+ it 'runs without errors' do
# old_rev is an ancestor of new_rev
expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
@@ -767,22 +844,28 @@ describe Repository, models: true do
branch = 'feature-ff-target'
repository.add_branch(user, branch, old_rev)
- expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+ expect do
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
+ end.not_to raise_error
end
end
context 'when the update would remove commits from the target branch' do
- it 'raises an exception' do
- branch = 'master'
- old_rev = repository.find_branch(branch).dereferenced_target.sha
+ let(:branch) { 'master' }
+ let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha }
+ it 'raises an exception' do
# The 'master' branch is NOT an ancestor of new_rev.
expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
- repository.update_branch_with_hooks(user, branch) { new_rev }
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
end.to raise_error(Repository::CommitError)
end
end
@@ -792,7 +875,9 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -800,7 +885,6 @@ describe Repository, models: true do
context 'when target branch is different from source branch' do
before do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
- allow(repository).to receive(:update_ref!)
end
it 'expires branch cache' do
@@ -809,7 +893,10 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
+ GitOperationService.new(user, repository).
+ with_branch('new-feature') do
+ new_rev
+ end
end
end
@@ -826,8 +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!',
- 'Updates file content', 'master', false)
+ empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
+ message: 'Updates file content',
+ branch_name: 'master')
end
end
end
@@ -877,7 +965,7 @@ describe Repository, models: true do
end
it 'sets autocrlf to :input' do
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
expect(repository.raw_repository.autocrlf).to eq(:input)
end
@@ -892,7 +980,7 @@ describe Repository, models: true do
expect(repository.raw_repository).not_to receive(:autocrlf=).
with(:input)
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
end
end
end
@@ -954,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
@@ -962,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
@@ -1009,8 +1097,11 @@ describe Repository, models: true do
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
- repository.commit(merge_commit_id)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
@@ -1021,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
@@ -1057,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
@@ -1290,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
@@ -1388,9 +1479,10 @@ describe Repository, models: true do
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
- expect(repository.rugged.tags).to receive(:delete).with('v1.1.0')
- repository.rm_tag('v1.1.0')
+ repository.rm_tag(create(:user), 'v1.1.0')
+
+ expect(repository.find_tag('v1.1.0')).to be_nil
end
end
@@ -1458,16 +1550,16 @@ describe Repository, models: true do
end
end
- describe '#update_ref!' do
+ describe '#update_ref' do
it 'can create a ref' do
- repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end
it 'raises CommitError when the ref update fails' do
expect do
- repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end
end
@@ -1645,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
@@ -1700,4 +1815,40 @@ describe Repository, models: true do
repository.refresh_method_caches(%i(readme license))
end
end
+
+ describe '#gitlab_ci_yml_for' do
+ before do
+ 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
+ it 'returns the content' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
+
+ describe '#route_map_for' do
+ before do
+ 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
+ it 'returns the content' do
+ expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index dd2a5109abc..0b222022e62 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group, path: 'gitlab') }
+ let!(:group) { create(:group, path: 'gitlab', name: 'gitlab') }
let!(:route) { group.route }
describe 'relationships' do
@@ -15,17 +15,42 @@ describe Route, models: true do
end
describe '#rename_descendants' do
- let!(:nested_group) { create(:group, path: "test", parent: group) }
- let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
- let!(:similar_group) { create(:group, path: 'gitlab-org') }
+ let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
+ let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }
+ let!(:similar_group) { create(:group, path: 'gitlab-org', name: 'gitlab-org') }
- before { route.update_attributes(path: 'bar') }
+ context 'path update' do
+ context 'when route name is set' do
+ before { route.update_attributes(path: 'bar') }
- it "updates children routes with new path" do
- expect(described_class.exists?(path: 'bar')).to be_truthy
- expect(described_class.exists?(path: 'bar/test')).to be_truthy
- expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
- expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
+ it "updates children routes with new path" do
+ expect(described_class.exists?(path: 'bar')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
+ expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
+ end
+ end
+
+ context 'when route name is nil' do
+ before do
+ route.update_column(:name, nil)
+ end
+
+ it "does not fail" do
+ expect(route.update_attributes(path: 'bar')).to be_truthy
+ end
+ end
+ end
+
+ context 'name update' do
+ before { route.update_attributes(name: 'bar') }
+
+ it "updates children routes with new path" do
+ 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
end
end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index f08935b6425..ebc694213b6 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -2,9 +2,37 @@ require 'rails_helper'
RSpec.describe Timelog, type: :model do
subject { build(:timelog) }
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) }
+
+ describe 'Issuable validation' do
+ it 'is invalid if issue_id and merge_request_id are missing' do
+ subject.attributes = { issue: nil, merge_request: nil }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is invalid if issue_id and merge_request_id are set' do
+ subject.attributes = { issue: issue, merge_request: merge_request }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is valid if only issue_id is set' do
+ subject.attributes = { issue: issue, merge_request: nil }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is valid if only merge_request_id is set' do
+ subject.attributes = { merge_request: merge_request, issue: nil }
+
+ expect(subject).to be_valid
+ 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 6ca5ad747d1..9da4140f3ce 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -19,21 +19,24 @@ describe User, models: true do
it { is_expected.to have_many(:project_members).dependent(:destroy) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
+ 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(:destroy) }
+ it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
+ it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_one(:abuse_report) }
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
@@ -141,6 +144,11 @@ describe User, models: true do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
end
+
+ it 'accepts example@test.com when added by another user' do
+ user = build(:user, email: "example@test.com", created_by_id: 1)
+ expect(user).to be_valid
+ end
end
context 'domain blacklist' do
@@ -159,6 +167,11 @@ describe User, models: true do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
end
+
+ it 'accepts info@example.com when added by another user' do
+ user = build(:user, email: 'info@example.com', created_by_id: 1)
+ expect(user).to be_valid
+ end
end
context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
@@ -293,6 +306,34 @@ describe User, models: true do
end
end
+ shared_context 'user keys' do
+ let(:user) { create(:user) }
+ let!(:key) { create(:key, user: user) }
+ let!(:deploy_key) { create(:deploy_key, user: user) }
+ end
+
+ describe '#keys' do
+ include_context 'user keys'
+
+ context 'with key and deploy key stored' do
+ it 'returns stored key, but not deploy_key' do
+ expect(user.keys).to include key
+ expect(user.keys).not_to include deploy_key
+ end
+ end
+ end
+
+ describe '#deploy_keys' do
+ include_context 'user keys'
+
+ context 'with key and deploy key stored' do
+ it 'returns stored deploy key, but not normal key' do
+ expect(user.deploy_keys).to include deploy_key
+ expect(user.deploy_keys).not_to include key
+ end
+ end
+ end
+
describe '#confirm' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
@@ -543,18 +584,16 @@ describe User, models: true do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
- expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
describe 'with default overrides' do
- let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
+ let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) }
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
- expect(user.theme_id).to eq(1)
end
end
@@ -656,12 +695,11 @@ 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) { create(:email, user: another_user) }
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
@@ -1013,8 +1051,8 @@ describe User, models: true do
let!(:project2) { create(:empty_project, forked_from_project: project3) }
let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) }
- let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) }
+ let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
+ let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
project1.team << [subject, :master]
@@ -1058,7 +1096,7 @@ describe User, models: true do
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
+ let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
@@ -1086,7 +1124,7 @@ describe User, models: true do
expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
- push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+ push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
@@ -1232,7 +1270,7 @@ describe User, models: true do
end
it 'does not include projects for which issues are disabled' do
- project = create(:empty_project, issues_access_level: ProjectFeature::DISABLED)
+ project = create(:empty_project, :issues_disabled)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
@@ -1378,7 +1416,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) }
@@ -1393,7 +1431,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
@@ -1422,4 +1460,74 @@ describe User, models: true do
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
end
end
+
+ describe '#access_level=' do
+ let(:user) { build(:user) }
+
+ it 'does nothing for an invalid access level' do
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:regular)
+ expect(user.admin).to be false
+ end
+
+ it "assigns the 'admin' access level" do
+ user.access_level = :admin
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "doesn't clear existing access levels when an invalid access level is passed in" do
+ user.access_level = :admin
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "accepts string values in addition to symbols" do
+ user.access_level = 'admin'
+
+ expect(user.access_level).to eq(:admin)
+ 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/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
new file mode 100644
index 00000000000..1caaa557085
--- /dev/null
+++ b/spec/models/wiki_directory_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+RSpec.describe WikiDirectory, models: true do
+ describe 'validations' do
+ subject { build(:wiki_directory) }
+
+ it { is_expected.to validate_presence_of(:slug) }
+ end
+
+ describe '#initialize' do
+ context 'when there are pages' do
+ let(:pages) { [build(:wiki_page)] }
+ let(:directory) { WikiDirectory.new('/path_up_to/dir', pages) }
+
+ it 'sets the slug attribute' do
+ expect(directory.slug).to eq('/path_up_to/dir')
+ end
+
+ it 'sets the pages attribute' do
+ expect(directory.pages).to eq(pages)
+ end
+ end
+
+ context 'when there are no pages' do
+ let(:directory) { WikiDirectory.new('/path_up_to/dir') }
+
+ it 'sets the slug attribute' do
+ expect(directory.slug).to eq('/path_up_to/dir')
+ end
+
+ it 'sets the pages attribute to an empty array' do
+ expect(directory.pages).to eq([])
+ end
+ end
+ end
+
+ describe '#to_partial_path' do
+ it 'returns the relative path to the partial to be used' do
+ directory = build(:wiki_directory)
+
+ expect(directory.to_partial_path).to eq('projects/wikis/wiki_directory')
+ end
+ end
+end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 5c34b1b0a30..753dc938c52 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -7,6 +7,75 @@ describe WikiPage, models: true do
subject { WikiPage.new(wiki) }
+ describe '.group_by_directory' do
+ context 'when there are no pages' do
+ it 'returns an empty array' do
+ expect(WikiPage.group_by_directory(nil)).to eq([])
+ expect(WikiPage.group_by_directory([])).to eq([])
+ end
+ end
+
+ context 'when there are pages' do
+ before do
+ create_page('dir_1/dir_1_1/page_3', 'content')
+ create_page('dir_1/page_2', 'content')
+ create_page('dir_2/page_5', 'content')
+ create_page('dir_2/page_4', 'content')
+ create_page('page_1', 'content')
+ end
+ let(:page_1) { wiki.find_page('page_1') }
+ let(:dir_1) do
+ WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
+ end
+ let(:dir_1_1) do
+ WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')])
+ end
+ let(:dir_2) do
+ pages = [wiki.find_page('dir_2/page_5'),
+ wiki.find_page('dir_2/page_4')]
+ WikiDirectory.new('dir_2', pages)
+ end
+
+ it 'returns an array with pages and directories' do
+ expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2]
+
+ grouped_entries = WikiPage.group_by_directory(wiki.pages)
+
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
+
+ expect(slugs).to match_array(expected_slugs)
+ end
+ end
+
+ it 'returns an array sorted by alphabetical position' do
+ # Directories and pages within directories are sorted alphabetically.
+ # Pages at root come before everything.
+ expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3',
+ 'dir_2/page_4', 'dir_2/page_5']
+
+ grouped_entries = WikiPage.group_by_directory(wiki.pages)
+
+ actual_order =
+ grouped_entries.map do |page_or_dir|
+ get_slugs(page_or_dir)
+ end.
+ flatten
+ expect(actual_order).to eq(expected_order)
+ end
+ end
+ end
+
+ describe '.unhyphenize' do
+ it 'removes hyphens from a name' do
+ name = 'a-name--with-hyphens'
+
+ expect(WikiPage.unhyphenize(name)).to eq('a name with hyphens')
+ end
+ end
+
describe "#initialize" do
context "when initialized with an existing gollum page" do
before do
@@ -189,6 +258,26 @@ describe WikiPage, models: true do
end
end
+ describe '#directory' do
+ context 'when the page is at the root directory' do
+ it 'returns an empty string' do
+ create_page('file', 'content')
+ page = wiki.find_page('file')
+
+ expect(page.directory).to eq('')
+ end
+ end
+
+ context 'when the page is inside an actual directory' do
+ it 'returns the full directory hierarchy' do
+ create_page('dir_1/dir_1_1/file', 'content')
+ page = wiki.find_page('dir_1/dir_1_1/file')
+
+ expect(page.directory).to eq('dir_1/dir_1_1')
+ end
+ end
+ end
+
describe '#historical?' do
before do
create_page('Update', 'content')
@@ -221,6 +310,27 @@ describe WikiPage, models: true do
end
end
+ describe '#to_partial_path' do
+ it 'returns the relative path to the partial to be used' do
+ page = build(:wiki_page)
+
+ expect(page.to_partial_path).to eq('projects/wikis/wiki_page')
+ end
+ end
+
+ describe '#==' do
+ let(:original_wiki_page) { create(:wiki_page) }
+
+ it 'returns true for identical wiki page' do
+ expect(original_wiki_page).to eq(original_wiki_page)
+ end
+
+ it 'returns false for updated wiki page' do
+ updated_wiki_page = original_wiki_page.update("Updated content")
+ expect(original_wiki_page).not_to eq(updated_wiki_page)
+ end
+ end
+
private
def remove_temp_repo(path)
@@ -239,4 +349,12 @@ describe WikiPage, models: true do
page = wiki.wiki.paged(title)
wiki.wiki.delete_page(page, commit_details)
end
+
+ def get_slugs(page_or_dir)
+ if page_or_dir.is_a? WikiPage
+ [page_or_dir.slug]
+ else
+ page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : []
+ 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/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eeab9827d99..0a5edf35f59 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -10,61 +10,59 @@ describe ProjectPolicy, models: true do
let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
let(:guest_permissions) do
- [
- :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
- :read_milestone, :read_project_snippet, :read_project_member,
- :read_note, :create_project, :create_issue, :create_note,
- :upload_file
+ %i[
+ read_project read_board read_list read_wiki read_issue read_label
+ read_milestone read_project_snippet read_project_member
+ read_note create_project create_issue create_note
+ upload_file
]
end
let(:reporter_permissions) do
- [
- :download_code, :fork_project, :create_project_snippet, :update_issue,
- :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
- :read_container_image, :read_pipeline, :read_environment, :read_deployment,
- :read_merge_request, :download_wiki_code
+ %i[
+ download_code fork_project create_project_snippet update_issue
+ admin_issue admin_label admin_list read_commit_status read_build
+ read_container_image read_pipeline read_environment read_deployment
+ read_merge_request download_wiki_code
]
end
let(:team_member_reporter_permissions) do
- [
- :build_download_code, :build_read_container_image
- ]
+ %i[build_download_code build_read_container_image]
end
let(:developer_permissions) do
- [
- :admin_merge_request, :update_merge_request, :create_commit_status,
- :update_commit_status, :create_build, :update_build, :create_pipeline,
- :update_pipeline, :create_merge_request, :create_wiki, :push_code,
- :resolve_note, :create_container_image, :update_container_image,
- :create_environment, :create_deployment
+ %i[
+ admin_merge_request update_merge_request create_commit_status
+ update_commit_status create_build update_build create_pipeline
+ update_pipeline create_merge_request create_wiki push_code
+ resolve_note create_container_image update_container_image
+ create_environment create_deployment
]
end
let(:master_permissions) do
- [
- :push_code_to_protected_branches, :update_project_snippet, :update_environment,
- :update_deployment, :admin_milestone, :admin_project_snippet,
- :admin_project_member, :admin_note, :admin_wiki, :admin_project,
- :admin_commit_status, :admin_build, :admin_container_image,
- :admin_pipeline, :admin_environment, :admin_deployment
+ %i[
+ push_code_to_protected_branches update_project_snippet update_environment
+ update_deployment admin_milestone admin_project_snippet
+ admin_project_member admin_note admin_wiki admin_project
+ admin_commit_status admin_build admin_container_image
+ admin_pipeline admin_environment admin_deployment
]
end
let(:public_permissions) do
- [
- :download_code, :fork_project, :read_commit_status, :read_pipeline,
- :read_container_image, :build_download_code, :build_read_container_image,
- :download_wiki_code
+ %i[
+ download_code fork_project read_commit_status read_pipeline
+ read_container_image build_download_code build_read_container_image
+ download_wiki_code
]
end
let(:owner_permissions) do
- [
- :change_namespace, :change_visibility_level, :rename_project, :remove_project,
- :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+ %i[
+ change_namespace change_visibility_level rename_project remove_project
+ archive_project remove_fork_project destroy_merge_request destroy_issue
]
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
new file mode 100644
index 00000000000..d0758af57dd
--- /dev/null
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe ProjectSnippetPolicy, models: true do
+ let(:current_user) { create(:user) }
+
+ let(:author_permissions) do
+ [
+ :update_project_snippet,
+ :admin_project_snippet
+ ]
+ end
+
+ subject { described_class.abilities(current_user, project_snippet).to_set }
+
+ context 'public snippet' do
+ let(:project_snippet) { create(:project_snippet, :public) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'internal snippet' do
+ let(:project_snippet) { create(:project_snippet, :internal) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'private snippet' do
+ let(:project_snippet) { create(:project_snippet, :private) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'snippet author' do
+ let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+
+ context 'project team member' do
+ before { project_snippet.project.team << [current_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'admin user' do
+ let(:current_user) { create(:admin) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ 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 e487297748b..46edbd49b28 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -48,6 +48,7 @@ describe API::AccessRequests, api: true do
get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
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)
end
@@ -199,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
@@ -209,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 c8e8f31cc1f..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,9 +31,10 @@ 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
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(downvote.name)
end
@@ -56,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
@@ -67,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
@@ -78,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)
@@ -87,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
@@ -95,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)
@@ -122,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
@@ -133,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
@@ -146,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')
@@ -154,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
@@ -172,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")
@@ -206,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)
@@ -214,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")
@@ -240,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
@@ -256,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
@@ -276,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
@@ -288,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 c14c3cb1ce7..87c36639cd4 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -55,6 +55,7 @@ describe API::Boards, api: true do
get api(base_url, user)
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(board.id)
@@ -72,6 +73,7 @@ describe API::Boards, api: true do
get api(base_url, user)
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(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
@@ -193,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 5a3ffc284f2..ab5a7e4d3de 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -17,8 +17,10 @@ describe API::Branches, api: true do
it "returns an array of project branches" do
project.repository.expire_all_method_caches
- get api("/projects/#{project.id}/repository/branches", user)
+ get api("/projects/#{project.id}/repository/branches", 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)
@@ -31,7 +33,18 @@ describe API::Branches, api: true do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
+ 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)
@@ -259,7 +272,7 @@ describe API::Branches, api: true do
describe "POST /projects/:id/repository/branches" do
it "creates a new branch" do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'feature1',
+ branch: 'feature1',
ref: branch_sha
expect(response).to have_http_status(201)
@@ -270,14 +283,14 @@ describe API::Branches, api: true do
it "denies for user without push access" do
post api("/projects/#{project.id}/repository/branches", user2),
- branch_name: branch_name,
+ branch: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
end
it 'returns 400 if branch name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new design',
+ branch: 'new design',
ref: branch_sha
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch name is invalid')
@@ -285,12 +298,12 @@ describe API::Branches, api: true do
it 'returns 400 if branch already exists' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design1',
+ branch: 'new_design1',
ref: branch_sha
expect(response).to have_http_status(201)
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design1',
+ branch: 'new_design1',
ref: branch_sha
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch already exists')
@@ -298,7 +311,7 @@ describe API::Branches, api: true do
it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design3',
+ branch: 'new_design3',
ref: 'foo'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Invalid reference name')
@@ -312,15 +325,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_name']).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)
- expect(response).to have_http_status(200)
- expect(json_response['branch_name']).to eq("with.1.2.3")
+ expect(response).to have_http_status(204)
end
it 'returns 404 if branch not exists' do
@@ -347,9 +359,11 @@ 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
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 7c9078b2864..024fa66848c 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -25,6 +25,7 @@ describe API::BroadcastMessages, api: true do
get api('/broadcast_messages', admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_kind_of(Array)
expect(json_response.first.keys)
.to match_array(%w(id message starts_at ends_at color font active))
@@ -173,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 bd6e23ee769..00000000000
--- a/spec/requests/api/builds_spec.rb
+++ /dev/null
@@ -1,472 +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(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 builds' 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 builds for specific commit' do
- expect(response).to have_http_status(200)
- 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 builds' 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 builds' 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 build 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 build 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 'build 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 build artifacts' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include(download_headers)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific build artifacts' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- it 'does not return build 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 build' 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 build' do
- before do
- get path_for_ref(pipeline.ref, 'NOBUILD')
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'find proper build' 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 build 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 build 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 build' 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 build' do
- expect(response).to have_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not cancel build' 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 build' 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 build' do
- expect(response).to have_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not retry build' 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 'build is erasable' do
- let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
-
- it 'erases build 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 build' do
- expect(build.reload.erased_at).to be_truthy
- expect(build.reload.erased_by).to eq user
- end
- end
-
- context 'build 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 build' do
- let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
-
- it 'plays the build' 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 build' do
- it 'returns a status code 400, Bad Request' do
- expect(response).to have_http_status 400
- expect(response.body).to match("Unplayable Build")
- end
- end
- end
-end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 88361def3cf..d8b3cc041a5 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do
let!(:project) { create(:project, :repository) }
let(:commit) { project.repository.commit }
- let(:commit_status) { create(:commit_status, pipeline: pipeline) }
let(:guest) { create_user(:guest) }
let(:reporter) { create_user(:reporter) }
let(:developer) { create_user(:developer) }
let(:sha) { commit.id }
+ let(:commit_status) do
+ create(:commit_status, status: :pending, pipeline: pipeline)
+ end
+
describe "GET /projects/:id/repository/commits/:sha/statuses" do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
@@ -18,10 +21,6 @@ describe API::CommitStatuses, api: true do
let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') }
let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') }
- it_behaves_like 'a paginated resources' do
- let(:request) { get api(get_url, reporter) }
- end
-
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
@@ -42,6 +41,7 @@ describe API::CommitStatuses, api: true do
it 'returns latest commit statuses' do
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
json_response.sort_by!{ |status| status['id'] }
@@ -54,7 +54,7 @@ describe API::CommitStatuses, api: true do
it 'returns all commit statuses' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status1.id, status2.id,
status3.id, status4.id,
@@ -67,7 +67,7 @@ describe API::CommitStatuses, api: true do
it 'returns latest commit statuses for specific ref' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status5.id)
end
@@ -78,7 +78,7 @@ describe API::CommitStatuses, api: true do
it 'return latest commit statuses for specific name' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status4.id, status5.id)
end
@@ -151,24 +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',
- target_url: 'http://gitlab.com/status' }
+ 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'
+ }
- post api(post_url, developer), optional_params
+ 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['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 af9028a8978..585449e62b6 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
@@ -72,18 +107,73 @@ describe API::Commits, api: true do
get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
expect(response).to have_http_status(400)
- expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format"
+ expect(json_response['error']).to eq('since is invalid')
end
end
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
@@ -107,7 +197,7 @@ describe API::Commits, api: true do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -120,7 +210,7 @@ describe API::Commits, api: true do
end
let!(:valid_c_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -148,7 +238,7 @@ describe API::Commits, api: true do
end
context 'with project path in URL' do
- let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+ let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
@@ -162,7 +252,7 @@ describe API::Commits, api: true do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
- branch_name: 'markdown',
+ branch: 'markdown',
commit_message: message,
actions: [
{
@@ -174,7 +264,7 @@ describe API::Commits, api: true do
end
let!(:valid_d_params) do
{
- branch_name: 'markdown',
+ branch: 'markdown',
commit_message: message,
actions: [
{
@@ -203,7 +293,7 @@ describe API::Commits, api: true do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
- branch_name: 'feature',
+ branch: 'feature',
commit_message: message,
actions: [
{
@@ -217,7 +307,7 @@ describe API::Commits, api: true do
end
let!(:valid_m_params) do
{
- branch_name: 'feature',
+ branch: 'feature',
commit_message: message,
actions: [
{
@@ -248,7 +338,7 @@ describe API::Commits, api: true do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -261,7 +351,7 @@ describe API::Commits, api: true do
end
let!(:valid_u_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -291,7 +381,7 @@ describe API::Commits, api: true do
let(:message) { 'Multiple actions' }
let!(:invalid_mo_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -319,7 +409,7 @@ describe API::Commits, api: true do
end
let!(:valid_mo_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -367,11 +457,21 @@ describe API::Commits, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(project.repository.commit.id)
- expect(json_response['title']).to eq(project.repository.commit.title)
- expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
- expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
- expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
+ commit = project.repository.commit
+ expect(json_response['id']).to eq(commit.id)
+ expect(json_response['short_id']).to eq(commit.short_id)
+ expect(json_response['title']).to eq(commit.title)
+ expect(json_response['message']).to eq(commit.safe_message)
+ expect(json_response['author_name']).to eq(commit.author_name)
+ expect(json_response['author_email']).to eq(commit.author_email)
+ expect(json_response['authored_date']).to eq(commit.authored_date.iso8601(3))
+ expect(json_response['committer_name']).to eq(commit.committer_name)
+ expect(json_response['committer_email']).to eq(commit.committer_email)
+ expect(json_response['committed_date']).to eq(commit.committed_date.iso8601(3))
+ expect(json_response['parent_ids']).to eq(commit.parent_ids)
+ expect(json_response['stats']['additions']).to eq(commit.stats.additions)
+ expect(json_response['stats']['deletions']).to eq(commit.stats.deletions)
+ expect(json_response['stats']['total']).to eq(commit.stats.total)
end
it "returns a 404 error if not found" do
@@ -446,6 +546,7 @@ describe API::Commits, api: true do
it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
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(2)
expect(json_response.first['note']).to eq('a comment on a commit')
@@ -464,6 +565,20 @@ describe API::Commits, api: true do
expect(response).to have_http_status(401)
end
end
+
+ context 'when the commit is present on two projects' do
+ let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) }
+ let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') }
+
+ it 'returns the comments for the target project' do
+ get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['note']).to eq('a comment on a commit for fork')
+ expect(json_response.first['author']['id']).to eq(user2.id)
+ end
+ end
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 766234d7104..4f4b18cf0e0 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -35,6 +35,7 @@ describe API::DeployKeys, api: true do
get api('/deploy_keys', admin)
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
@@ -48,6 +49,7 @@ describe API::DeployKeys, api: true do
get api("/projects/#{project.id}/deploy_keys", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
@@ -114,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
@@ -146,25 +150,4 @@ describe API::DeployKeys, api: true do
end
end
end
-
- describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do
- context 'when the user can admin the project' do
- it 'disables the key' do
- expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin)
- end.to change { project.deploy_keys.count }.from(1).to(0)
-
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(deploy_key.id)
- end
- end
-
- context 'when authenticated as non-admin user' do
- it 'should return a 404 error' do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user)
-
- expect(response).to have_http_status(404)
- end
- end
- end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 31e3cfa1b2f..e55575ffbda 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -14,14 +14,11 @@ describe API::Deployments, api: true do
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(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
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 8168b613766..b54ee8e8b85 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -14,19 +14,18 @@ describe API::Environments, api: true do
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/environments", user) }
- end
-
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)
+ expect(response).to include_pagination_headers
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'].keys).to contain_exactly(*project_data_keys)
end
end
@@ -125,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
@@ -144,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 5e26e779366..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 "POST /projects/:id/repository/files" do
+ 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/:file_path" do
+ let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
let(:valid_params) do
{
- file_path: 'newfile.rb',
- branch_name: '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,30 +237,43 @@ 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_name: 'master',
+ branch: 'master',
content: 'puts 8',
commit_message: 'Changed file'
}
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_name: 'master',
+ 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,22 +324,18 @@ 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_name: 'master',
+ branch: 'master',
content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
commit_message: 'Binary file with a \n should not be touched',
encoding: 'base64'
@@ -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/fork_spec.rb b/spec/requests/api/fork_spec.rb
deleted file mode 100644
index 92ac4fd334d..00000000000
--- a/spec/requests/api/fork_spec.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-require 'spec_helper'
-
-describe API::Projects, api: true do
- include ApiHelpers
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:admin) { create(:admin) }
- let(:group) { create(:group) }
- let(:group2) do
- group = create(:group, name: 'group2_name')
- group.add_owner(user2)
- group
- end
-
- describe 'POST /projects/fork/:id' do
- let(:project) do
- create(:project, :repository, creator: user, namespace: user.namespace)
- end
-
- before do
- project.add_reporter(user2)
- end
-
- context 'when authenticated' do
- it 'forks if user has sufficient access to project' do
- post api("/projects/fork/#{project.id}", user2)
-
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq(project.name)
- expect(json_response['path']).to eq(project.path)
- expect(json_response['owner']['id']).to eq(user2.id)
- expect(json_response['namespace']['id']).to eq(user2.namespace.id)
- expect(json_response['forked_from_project']['id']).to eq(project.id)
- end
-
- it 'forks if user is admin' do
- post api("/projects/fork/#{project.id}", admin)
-
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq(project.name)
- expect(json_response['path']).to eq(project.path)
- expect(json_response['owner']['id']).to eq(admin.id)
- expect(json_response['namespace']['id']).to eq(admin.namespace.id)
- expect(json_response['forked_from_project']['id']).to eq(project.id)
- end
-
- it 'fails on missing project access for the project to fork' do
- new_user = create(:user)
- post api("/projects/fork/#{project.id}", new_user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
-
- it 'fails if forked project exists in the user namespace' do
- post api("/projects/fork/#{project.id}", user)
-
- expect(response).to have_http_status(409)
- expect(json_response['message']['name']).to eq(['has already been taken'])
- expect(json_response['message']['path']).to eq(['has already been taken'])
- end
-
- it 'fails if project to fork from does not exist' do
- post api('/projects/fork/424242', user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
-
- it 'forks with explicit own user namespace id' do
- post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id
-
- expect(response).to have_http_status(201)
- expect(json_response['owner']['id']).to eq(user2.id)
- end
-
- it 'forks with explicit own user name as namespace' do
- post api("/projects/fork/#{project.id}", user2), namespace: user2.username
-
- expect(response).to have_http_status(201)
- expect(json_response['owner']['id']).to eq(user2.id)
- end
-
- it 'forks to another user when admin' do
- post api("/projects/fork/#{project.id}", admin), namespace: user2.username
-
- expect(response).to have_http_status(201)
- expect(json_response['owner']['id']).to eq(user2.id)
- end
-
- it 'fails if trying to fork to another user when not admin' do
- post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
-
- expect(response).to have_http_status(404)
- end
-
- it 'fails if trying to fork to non-existent namespace' do
- post api("/projects/fork/#{project.id}", user2), namespace: 42424242
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Target Namespace Not Found')
- end
-
- it 'forks to owned group' do
- post api("/projects/fork/#{project.id}", user2), namespace: group2.name
-
- expect(response).to have_http_status(201)
- expect(json_response['namespace']['name']).to eq(group2.name)
- end
-
- it 'fails to fork to not owned group' do
- post api("/projects/fork/#{project.id}", user2), namespace: group.name
-
- expect(response).to have_http_status(404)
- end
-
- it 'forks to not owned group when admin' do
- post api("/projects/fork/#{project.id}", admin), namespace: group.name
-
- expect(response).to have_http_status(201)
- expect(json_response['namespace']['name']).to eq(group.name)
- end
- end
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- post api("/projects/fork/#{project.id}")
-
- expect(response).to have_http_status(401)
- expect(json_response['message']).to eq('401 Unauthorized')
- end
- end
- end
-end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index a027c23bb88..2545da7b1db 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -33,15 +33,18 @@ describe API::Groups, api: true do
get api("/groups", user1)
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['name']).to eq(group1.name)
+ expect(json_response)
+ .to satisfy_one { |group| group['name'] == group1.name }
end
it "does not include statistics" do
get api("/groups", user1), statistics: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
@@ -52,6 +55,7 @@ describe API::Groups, api: true do
get api("/groups", admin)
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(2)
end
@@ -60,6 +64,7 @@ describe API::Groups, api: true do
get api("/groups", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -70,15 +75,19 @@ describe API::Groups, api: true do
repository_size: 123,
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)
get api("/groups", admin), statistics: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ expect(json_response)
+ .to satisfy_one { |group| group['statistics'] == exposed_attributes }
end
end
@@ -87,6 +96,7 @@ describe API::Groups, api: true do
get api("/groups", admin), skip_groups: [group2.id]
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)
end
@@ -101,6 +111,7 @@ describe API::Groups, api: true do
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to contain_exactly(public_group.name, group1.name)
end
@@ -118,6 +129,7 @@ describe API::Groups, api: true do
get api("/groups", user1)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group3.name, group1.name])
end
@@ -126,6 +138,7 @@ describe API::Groups, api: true do
get api("/groups", user1), sort: "desc"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
end
@@ -134,26 +147,18 @@ describe API::Groups, api: true do
get api("/groups", user1), order_by: "path"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 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
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
end
@@ -173,12 +178,13 @@ 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)
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
@@ -287,20 +293,22 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user1)
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
get api("/groups/#{group1.id}/projects", user1), simple: true
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
@@ -309,6 +317,7 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
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['name']).to eq(public_project.name)
@@ -332,9 +341,30 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user3)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 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 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
@@ -342,6 +372,7 @@ describe API::Groups, api: true do
get api("/groups/#{group2.id}/projects", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
@@ -358,7 +389,8 @@ describe API::Groups, api: true do
get api("/groups/#{group1.path}/projects", admin)
expect(response).to have_http_status(200)
- project_names = json_response.map { |proj| proj['name' ] }
+ expect(response).to include_pagination_headers
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
end
@@ -398,6 +430,19 @@ describe API::Groups, api: true do
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 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 api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
@@ -424,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
@@ -453,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
@@ -466,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..f18b8e98707 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -409,6 +409,34 @@ describe API::Internal, api: true do
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)
double().tap do |fake_project|
allow(fake_project).to receive_message_chain('repository.path_to_repo' => path)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 62f1b8d7ca2..de7dbca0b22 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -68,7 +68,9 @@ describe API::Issues, api: true do
context "when authenticated" do
it "returns an array of issues" do
get api("/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.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
@@ -76,7 +78,9 @@ describe API::Issues, api: true do
it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
+
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)
@@ -84,7 +88,9 @@ describe API::Issues, api: true do
it 'returns an array of opened issues' do
get api('/issues?state=opened', user)
+
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)
@@ -92,7 +98,9 @@ describe API::Issues, api: true do
it 'returns an array of all issues' do
get api('/issues?state=all', user)
+
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(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -101,31 +109,44 @@ describe API::Issues, api: true do
it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
+
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 eq([label.title])
end
- it 'returns an array of labeled issues when at least one label matches' do
- get api("/issues?labels=#{label.title},foo,bar", user)
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: project)
+ label_c = create(:label, title: 'bar', project: project)
+
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
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 eq([label.title])
+ expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
+
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 array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
+
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 eq([label.title])
@@ -134,7 +155,9 @@ describe API::Issues, api: true do
it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
+
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
@@ -143,6 +166,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{empty_milestone.title}", user)
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
@@ -151,6 +175,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=foo", user)
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
@@ -159,6 +184,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{milestone.title}", user)
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(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -170,6 +196,7 @@ describe API::Issues, api: true do
'&state=closed', user)
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)
@@ -179,46 +206,77 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{no_milestone_title}", author)
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(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)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api('/issues?sort=asc', user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get api('/issues?order_by=updated_at', user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get api('/issues?order_by=updated_at&sort=asc', user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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
@@ -269,6 +327,7 @@ describe API::Issues, api: true do
get api(base_url, non_member)
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['title']).to eq(group_issue.title)
@@ -278,6 +337,7 @@ describe API::Issues, api: true do
get api(base_url, author)
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(2)
end
@@ -286,6 +346,7 @@ describe API::Issues, api: true do
get api(base_url, assignee)
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(2)
end
@@ -294,6 +355,7 @@ describe API::Issues, api: true do
get api(base_url, user)
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(2)
end
@@ -302,6 +364,7 @@ describe API::Issues, api: true do
get api(base_url, admin)
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(2)
end
@@ -310,6 +373,7 @@ describe API::Issues, api: true do
get api("#{base_url}?labels=#{group_label.title}", user)
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 eq([group_label.title])
@@ -319,6 +383,41 @@ describe API::Issues, api: true do
get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
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 array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: group_project)
+ label_c = create(:label, title: 'bar', project: group_project)
+
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
+
+ get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
+
+ 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['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
@@ -327,6 +426,7 @@ describe API::Issues, api: true do
get api("#{base_url}?labels=foo,bar", user)
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
@@ -335,6 +435,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
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
@@ -343,6 +444,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=foo", user)
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
@@ -351,6 +453,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{group_milestone.title}", user)
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)
@@ -361,6 +464,7 @@ describe API::Issues, api: true do
'&state=closed', user)
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_closed_issue.id)
@@ -370,6 +474,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{no_milestone_title}", user)
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_confidential_issue.id)
@@ -377,36 +482,40 @@ describe API::Issues, api: true do
it 'sorts by created_at descending by default' do
get api(base_url, user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api("#{base_url}?sort=asc", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get api("#{base_url}?order_by=updated_at&sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -425,17 +534,22 @@ describe API::Issues, api: true do
end
it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ restricted_project = create(:empty_project, :public, :issues_private)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response).to eq([])
end
it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
+
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(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -443,7 +557,9 @@ describe API::Issues, api: true do
it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
+
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(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -451,7 +567,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
+
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(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -459,7 +577,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
+
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(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -467,7 +587,9 @@ describe API::Issues, api: true do
it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/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.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -475,7 +597,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
+
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(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -483,38 +607,80 @@ describe API::Issues, api: true do
it 'returns an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
+
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 eq([label.title])
end
- it 'returns an array of labeled project issues where all labels match' do
- get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: project)
+ label_c = create(:label, title: 'bar', project: project)
+
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
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 eq([label.title])
+ 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)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
end
it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
+
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 issue matches milestone' do
get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
+
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 milestone does not exist' do
get api("#{base_url}/issues?milestone=foo", user)
+
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
@@ -523,6 +689,7 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?milestone=#{milestone.title}", user)
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(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -530,9 +697,10 @@ describe API::Issues, api: true do
end
it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}/issues?milestone=#{milestone.title}"\
- '&state=closed', user)
+ get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user)
+
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)
@@ -542,6 +710,7 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
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(confidential_issue.id)
@@ -549,44 +718,48 @@ describe API::Issues, api: true do
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api("#{base_url}/issues?sort=asc", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}/issues?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
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)
@@ -604,70 +777,65 @@ 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)
expect(json_response['iid']).to eq(issue.iid)
end
- it 'returns a project issue by iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 1
- expect(json_response.first['title']).to eq issue.title
- expect(json_response.first['id']).to eq issue.id
- expect(json_response.first['iid']).to eq issue.iid
+ it "returns 404 if issue id not found" do
+ get api("/projects/#{project.id}/issues/54321", user)
+ expect(response).to have_http_status(404)
end
- it 'returns an empty array for an unknown project issue iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 0
- end
+ it "returns 404 if the issue ID is used" do
+ get api("/projects/#{project.id}/issues/#{issue.id}", user)
- it "returns 404 if issue id not found" do
- get api("/projects/#{project.id}/issues/54321", 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)
@@ -683,7 +851,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
@@ -760,29 +928,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
- it 'resolves the discussions in a merge request' do
- discussion.first_note.reload
+ 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
- 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
@@ -848,23 +1021,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, ?, &'
@@ -878,40 +1057,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)
@@ -919,7 +1098,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)
@@ -927,7 +1106,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)
@@ -936,12 +1115,39 @@ 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 with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" 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.iid}", user), params
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('updated title')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ 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])
@@ -952,7 +1158,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
@@ -960,14 +1166,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'
@@ -975,7 +1181,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'
@@ -989,7 +1195,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([
@@ -998,9 +1204,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)
@@ -1009,7 +1215,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'
@@ -1018,7 +1224,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)
@@ -1028,25 +1234,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
@@ -1055,9 +1261,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
@@ -1068,14 +1274,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)
@@ -1084,7 +1296,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)
@@ -1094,7 +1306,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)
@@ -1103,13 +1315,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),
@@ -1122,7 +1344,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)
@@ -1132,7 +1354,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)
@@ -1140,55 +1362,67 @@ describe API::Issues, api: true do
end
end
- describe 'POST :id/issues/:issue_id/subscription' do
+ describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscription", 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}/subscription", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/subscription", user)
+ post api("/projects/#{project.id}/issues/123/subscribe", user)
+
+ 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}/subscription", non_member)
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
expect(response).to have_http_status(404)
end
end
- describe 'DELETE :id/issues/:issue_id/subscription' do
+ describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
- delete api("/projects/#{project.id}/issues/123/subscription", user)
+ post api("/projects/#{project.id}/issues/123/unsubscribe", user)
+
+ 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
- delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", 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 a8cd787f398..a1adaba7b98 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -21,15 +21,16 @@ 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)
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(3)
expect(json_response.first.keys).to match_array expected_keys
@@ -174,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
@@ -317,10 +319,10 @@ describe API::Labels, api: true do
end
end
- describe "POST /projects/:id/labels/:label_id/subscription" do
+ describe "POST /projects/:id/labels/:label_id/subscribe" do
context "when label_id is a label title" do
it "subscribes to the label" do
- post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
@@ -330,7 +332,7 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do
it "subscribes to the label" do
- post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
@@ -342,7 +344,7 @@ describe API::Labels, api: true do
before { label1.subscribe(user, project) }
it "returns 304" do
- post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(304)
end
@@ -350,21 +352,21 @@ describe API::Labels, api: true do
context "when label ID is not found" do
it "returns 404 error" do
- post api("/projects/#{project.id}/labels/1234/subscription", user)
+ post api("/projects/#{project.id}/labels/1234/subscribe", user)
expect(response).to have_http_status(404)
end
end
end
- describe "DELETE /projects/:id/labels/:label_id/subscription" do
+ describe "POST /projects/:id/labels/:label_id/unsubscribe" do
before { label1.subscribe(user, project) }
context "when label_id is a label title" do
it "unsubscribes from the label" do
- delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -372,9 +374,9 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do
it "unsubscribes from the label" do
- delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -384,7 +386,7 @@ describe API::Labels, api: true do
before { label1.unsubscribe(user, project) }
it "returns 304" do
- delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
expect(response).to have_http_status(304)
end
@@ -392,7 +394,7 @@ describe API::Labels, api: true do
context "when label ID is not found" do
it "returns 404 error" do
- delete api("/projects/#{project.id}/labels/1234/subscription", user)
+ post api("/projects/#{project.id}/labels/1234/unsubscribe", user)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 9892e014cb9..2d37d026a39 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -34,9 +34,12 @@ describe API::Members, api: true do
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
+
get api("/#{source_type.pluralize}/#{source.id}/members", 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.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -49,6 +52,8 @@ describe API::Members, api: true do
get api("/#{source_type.pluralize}/#{source.id}/members", developer)
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 { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -57,6 +62,8 @@ describe API::Members, api: true do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(master.username)
end
@@ -145,11 +152,11 @@ describe API::Members, api: true do
end
end
- it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: master.id, access_level: Member::MASTER
- expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ expect(response).to have_http_status(409)
end
it 'returns 400 when user_id is not given' do
@@ -166,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
@@ -223,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
@@ -256,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
@@ -276,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
@@ -335,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 e1887138aab..79f3151ba52 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -13,27 +13,35 @@ 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
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
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
+ 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)
@@ -41,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 21a2c583aa8..9aba1d75612 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -27,7 +27,9 @@ describe API::MergeRequests, api: true do
context "when authenticated" do
it "returns an array of all merge_requests" do
get api("/projects/#{project.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.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -43,7 +45,9 @@ describe API::MergeRequests, api: true do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
+
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(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -51,7 +55,9 @@ describe API::MergeRequests, api: true do
it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
+
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.last['title']).to eq(merge_request.title)
@@ -59,7 +65,9 @@ describe API::MergeRequests, api: true do
it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
+
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['title']).to eq(merge_request_closed.title)
@@ -67,12 +75,31 @@ describe API::MergeRequests, api: true do
it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
+
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['title']).to eq(merge_request_merged.title)
end
+ it 'returns merge_request by "iids" array' do
+ get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.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.first['title']).to eq merge_request_closed.title
+ 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
@@ -81,7 +108,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
+
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(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -90,7 +119,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
+
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(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -99,7 +130,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+
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(3)
response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
@@ -108,7 +141,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+
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(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -118,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)
@@ -142,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)
@@ -159,26 +194,14 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'returns merge_request by iid' do
- url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
- get api(url, user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq merge_request.title
- expect(json_response.first['id']).to eq merge_request.id
+ 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 merge_request by iid array' do
- get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.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.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
- 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)
- it "returns a 404 error if merge_request_id not found" do
- get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
end
@@ -186,41 +209,56 @@ describe API::MergeRequests, api: true 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
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(merge_request.commits.size)
expect(json_response.first['id']).to eq(commit.id)
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
@@ -237,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
@@ -380,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) }
@@ -389,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
@@ -416,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')
@@ -424,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
@@ -439,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')
@@ -448,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
@@ -470,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')
@@ -489,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, ?, &'
@@ -533,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)
@@ -541,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')
@@ -563,24 +637,32 @@ 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
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq("a comment on a MR")
@@ -588,29 +670,38 @@ 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
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 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
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -621,9 +712,10 @@ 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
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(issue.title)
@@ -636,28 +728,46 @@ 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/subscription' 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}/subscription", 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}/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
- post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/123/subscribe", user)
+
+ 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
@@ -666,28 +776,34 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
expect(response).to have_http_status(403)
end
end
- describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do
it 'unsubscribes from a merge request' do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
- post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user)
+
+ 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
@@ -696,7 +812,7 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", 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 8beef821d6c..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] }
@@ -14,6 +14,7 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones", 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['title']).to eq(milestone.title)
end
@@ -28,6 +29,7 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones?state=active", user)
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(milestone.id)
@@ -37,10 +39,30 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones?state=closed", user)
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_milestone.id)
end
+
+ 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
@@ -52,23 +74,46 @@ describe API::Milestones, api: true do
expect(json_response['iid']).to eq(milestone.iid)
end
- it 'returns a project milestone by iid' do
- get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
+ 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
+ expect(json_response.size).to eq(1)
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 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 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(1)
expect(json_response.first['title']).to eq milestone.title
expect(json_response.first['id']).to eq milestone.id
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 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
@@ -177,10 +222,18 @@ describe API::Milestones, api: true do
get api("/projects/#{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.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")
@@ -190,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]
@@ -202,6 +255,7 @@ describe API::Milestones, api: true 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.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
@@ -214,6 +268,7 @@ describe API::Milestones, api: true do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
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.map { |issue| issue['id'] }).to include(issue.id)
@@ -223,10 +278,73 @@ describe API::Milestones, api: true do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(: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(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, position: 2) }
+ let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) }
+
+ before do
+ milestone.merge_requests << merge_request
+ end
+
+ it 'returns project merge_requests for a particular milestone' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", 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['title']).to eq(merge_request.title)
+ expect(json_response.first['milestone']['title']).to eq(milestone.title)
+ end
+
+ it 'returns a 404 error if milestone id not found' do
+ get api("/projects/#{project.id}/milestones/1234/merge_requests", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 if the user has no access to the milestone' do
+ new_user = create :user
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 401 error if user not authenticated' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests")
+
+ 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/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index c1edf384d5c..da8fa06d0af 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -5,7 +5,7 @@ describe API::Namespaces, api: true do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let!(:group1) { create(:group) }
- let!(:group2) { create(:group) }
+ let!(:group2) { create(:group, :nested) }
describe "GET /namespaces" do
context "when unauthenticated" do
@@ -18,35 +18,41 @@ describe API::Namespaces, api: true do
context "when authenticated as admin" do
it "admin: returns an array of all namespaces" do
get api("/namespaces", admin)
+
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(Namespace.count)
end
it "admin: returns an array of matched namespaces" do
- get api("/namespaces?search=#{group1.name}", admin)
+ get api("/namespaces?search=#{group2.name}", admin)
+
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.last['path']).to eq(group2.path)
+ expect(json_response.last['full_path']).to eq(group2.full_path)
end
end
context "when authenticated as a regular user" do
it "user: returns an array of namespaces" do
get api("/namespaces", user)
+
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)
end
it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
+
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)
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 0353ebea9e5..347f8f6fa3b 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -32,15 +32,12 @@ describe API::Notes, api: true do
before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
- end
-
context "when noteable is an Issue" do
it "returns an array of issue notes" do
get 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)
end
@@ -56,6 +53,7 @@ describe API::Notes, api: true do
get 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
@@ -75,6 +73,7 @@ describe API::Notes, api: true do
get 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
@@ -87,6 +86,7 @@ describe API::Notes, api: true do
get 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
@@ -109,6 +109,7 @@ describe API::Notes, api: true do
get 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
@@ -224,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
@@ -372,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)
@@ -391,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)
@@ -411,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 b7a0b5a9e13..51af999b455 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -15,18 +15,16 @@ describe API::Pipelines, api: true do
before { project.team << [user, :master] }
describe 'GET /projects/:id/pipelines ' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/pipelines", user) }
- end
-
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", 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['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 f4973d71088..b1f8c249092 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -25,6 +25,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
+ expect(response).to include_pagination_headers
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)
@@ -32,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)
@@ -58,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)
@@ -97,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)
@@ -143,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)
@@ -182,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 45d5ae267c5..9e88c19b0bc 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -7,18 +7,6 @@ describe API::ProjectSnippets, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
- describe 'GET /projects/:project_id/snippets/:id' do
- # TODO (rspeicher): Deprecated; remove in 9.0
- it 'always exposes expires_at as nil' do
- snippet = create(:project_snippet, author: admin)
-
- get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
-
- expect(json_response).to have_key('expires_at')
- expect(json_response['expires_at']).to be_nil
- end
- end
-
describe 'GET /projects/:project_id/snippets/' do
let(:user) { create(:user) }
@@ -28,9 +16,11 @@ describe API::ProjectSnippets, api: true do
internal_snippet = create(:project_snippet, :internal, project: project)
private_snippet = create(:project_snippet, :private, project: project)
- get api("/projects/#{project.id}/snippets/", user)
+ get api("/projects/#{project.id}/snippets", 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(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
expect(json_response.last).to have_key('web_url')
@@ -40,7 +30,10 @@ describe API::ProjectSnippets, api: true do
create(:project_snippet, :private, project: project)
get api("/projects/#{project.id}/snippets/", 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(0)
end
end
@@ -51,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
@@ -63,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
@@ -85,43 +78,33 @@ describe API::ProjectSnippets, api: true do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
- context 'when the project is private' do
- let(:private_project) { create(:project_empty_repo, :private) }
-
- context 'when the snippet is public' do
- it 'creates the snippet' do
- expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
- to change { Snippet.count }.by(1)
- end
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility: 'private') }.
+ to change { Snippet.count }.by(1)
end
end
- context 'when the project is public' do
- context 'when the snippet is private' do
- it 'creates the snippet' do
- expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
- to change { Snippet.count }.by(1)
- end
+ context 'when the snippet is public' do
+ it 'rejects the snippet' do
+ expect { create_snippet(project, visibility: 'public') }.
+ not_to change { Snippet.count }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
- context 'when the snippet is public' do
- it 'rejects the shippet' do
- expect { create_snippet(project, 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(project, visibility_level: Snippet::PUBLIC) }.
- to change { SpamLog.count }.by(1)
- end
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility: 'public') }.
+ to change { SpamLog.count }.by(1)
end
end
end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
- let(:snippet) { create(:project_snippet, author: admin) }
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) }
it 'updates snippet' do
new_content = 'New content'
@@ -145,6 +128,56 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'creates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'DELETE /projects/:project_id/snippets/:id/' do
@@ -156,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
@@ -179,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 753dde0ca3a..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) }
@@ -41,261 +41,246 @@ describe API::Projects, api: true do
end
describe 'GET /projects' do
- before { project }
+ shared_examples_for 'projects response' do
+ it 'returns an array of projects' do
+ 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
+ end
+
+ let!(:public_project) { create(:empty_project, :public, name: 'public_project') }
+ before do
+ project
+ project2
+ project3
+ project4
+ end
context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/projects')
- expect(response).to have_http_status(401)
+ it_behaves_like 'projects response' do
+ let(:filter) { {} }
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
end
end
context 'when authenticated as regular user' do
- it 'returns an array of projects' do
- get api('/projects', user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(project.name)
- expect(json_response.first['owner']['username']).to eq(user.username)
+ it_behaves_like 'projects response' do
+ let(:filter) { {} }
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
end
it 'includes the project labels as the tag_list' do
get api('/projects', user)
+
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('tag_list')
end
it 'includes open_issues_count' do
get api('/projects', user)
+
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
end
- it 'does not include open_issues_count' do
+ it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects', user)
+
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
+ end
+
+ it "does not include statistics by default" do
+ get api('/projects', 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.keys).not_to include('open_issues_count')
+ expect(json_response.first).not_to include('statistics')
end
- context 'GET /projects?simple=true' do
+ it "includes statistics if requested" do
+ get api('/projects', user), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include 'statistics'
+ end
+
+ 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)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to match_array expected_keys
end
end
context 'and using search' do
- it 'returns searched project' do
- get api('/projects', user), { search: project.name }
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ it_behaves_like 'projects response' do
+ let(:filter) { { search: project.name } }
+ let(:current_user) { user }
+ let(:projects) { [project] }
+ end
+ end
+
+ 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
context 'and using the visibility filter' do
it 'filters based on private visibility param' do
get api('/projects', user), { visibility: 'private' }
+
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(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id)
end
it 'filters based on internal visibility param' do
+ project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)
+
get api('/projects', user), { visibility: 'internal' }
+
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(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id)
end
it 'filters based on public visibility param' do
get api('/projects', user), { visibility: 'public' }
+
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(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
end
end
context 'and using sorting' do
- before do
- project2
- project3
- end
-
it 'returns the correct order when sorted by id' do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
end
- end
- end
-
- describe 'GET /projects/all' do
- before { project }
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/projects/all')
- expect(response).to have_http_status(401)
- end
- end
- context 'when authenticated as regular user' do
- it 'returns authentication error' do
- get api('/projects/all', user)
- expect(response).to have_http_status(403)
- end
- end
-
- context 'when authenticated as admin' do
- it 'returns an array of all projects' do
- get api('/projects/all', admin)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
+ context 'and with owned=true' do
+ it 'returns an array of projects the user owns' do
+ get api('/projects', user4), owned: true
- expect(json_response).to satisfy do |response|
- response.one? do |entry|
- entry.has_key?('permissions') &&
- entry['name'] == project.name &&
- entry['owner']['username'] == user.username
- end
+ 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(project4.name)
+ expect(json_response.first['owner']['username']).to eq(user4.username)
end
end
- it "does not include statistics by default" do
- get api('/projects/all', 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
- get api('/projects/all', admin), statistics: true
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first).to include 'statistics'
- end
- end
- end
-
- describe 'GET /projects/owned' do
- before do
- project3
- project4
- end
+ context 'and with starred=true' do
+ let(:public_project) { create(:empty_project, :public) }
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/projects/owned')
- expect(response).to have_http_status(401)
- end
- end
-
- context 'when authenticated as project owner' do
- it 'returns an array of projects the user owns' do
- get api('/projects/owned', user4)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(project4.name)
- expect(json_response.first['owner']['username']).to eq(user4.username)
- end
+ before do
+ project_member2
+ user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ end
- it "does not include statistics by default" do
- get api('/projects/owned', user4)
+ it 'returns the starred projects viewable by the user' do
+ get api('/projects', user3), starred: true
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first).not_to include('statistics')
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
+ end
end
- it "includes statistics if requested" do
- attributes = {
- commit_count: 23,
- storage_size: 702,
- repository_size: 123,
- lfs_objects_size: 234,
- build_artifacts_size: 345,
- }
-
- project4.statistics.update!(attributes)
-
- get api('/projects/owned', user4), statistics: true
+ context 'and with all query parameters' do
+ 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') }
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['statistics']).to eq attributes.stringify_keys
- end
- end
- end
+ before do
+ user.update_attributes(starred_projects: [project5, project7, project8, project9])
+ end
- describe 'GET /projects/visible' do
- shared_examples_for 'visible projects response' do
- it 'returns the visible projects' do
- get api('/projects/visible', current_user)
+ 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(json_response).to be_an Array
- expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
- 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.size).to eq(1)
+ expect(json_response.first['id']).to eq(project7.id)
+ end
+ end
- let!(:public_project) { create(:empty_project, :public) }
- before do
- project
- project2
- project3
- project4
- end
+ context 'including membership filter' do
+ before do
+ create(:project_member,
+ user: user,
+ project: project5,
+ access_level: ProjectMember::MASTER)
+ end
- context 'when unauthenticated' do
- it_behaves_like 'visible projects response' do
- let(:current_user) { nil }
- let(:projects) { [public_project] }
- end
- end
+ it 'returns only projects that satisfy all query parameters' do
+ get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
- context 'when authenticated' do
- it_behaves_like 'visible projects response' do
- let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ 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 'visible projects response' do
+ it_behaves_like 'projects response' do
+ let(:filter) { {} }
let(:current_user) { user2 }
let(:projects) { [public_project] }
end
end
- end
-
- describe 'GET /projects/starred' do
- let(:public_project) { create(:empty_project, :public) }
-
- before do
- project_member2
- user3.update_attributes(starred_projects: [project, project2, project3, public_project])
- end
- it 'returns the starred projects viewable by the user' do
- get api('/projects/starred', user3)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
+ context 'when authenticated as admin' do
+ it_behaves_like 'projects response' do
+ let(:filter) { {} }
+ let(:current_user) { admin }
+ let(:projects) { Project.all }
+ end
end
end
@@ -309,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
@@ -321,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
@@ -333,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
})
@@ -353,57 +365,39 @@ describe API::Projects, api: true do
end
it 'sets a project as public' do
- project = attributes_for(:project, :public)
- post api('/projects', user), project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
+ project = attributes_for(:project, visibility: 'public')
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
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)
- post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
+ project = attributes_for(:project, visibility: 'internal')
- it 'sets a project as internal overriding :public' do
- project = attributes_for(:project, :internal, { public: true })
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)
- post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
+ project = attributes_for(:project, visibility: 'private')
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
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
@@ -430,14 +424,23 @@ 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, visibility: 'public') }
+
before do
- @project = attributes_for(:project, { public: true })
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'does not allow a non-admin to use a restricted visibility level' do
- post api('/projects', user), @project
+ post api('/projects', user), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
@@ -446,11 +449,9 @@ describe API::Projects, api: true do
end
it 'allows an admin to override restricted visibility settings' do
- post api('/projects', admin), @project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to(
- eq(Gitlab::VisibilityLevel::PUBLIC)
- )
+ post api('/projects', admin), project_param
+
+ expect(json_response['visibility']).to eq('public')
end
end
end
@@ -491,64 +492,41 @@ describe API::Projects, api: true do
end
it 'sets a project as public' do
- project = attributes_for(:project, :public)
- post api("/projects/user/#{user.id}", admin), project
+ project = attributes_for(:project, visibility: 'public')
- expect(response).to have_http_status(201)
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
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)
- 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)
- end
+ project = attributes_for(:project, visibility: 'internal')
- it 'sets a project as internal overriding :public' do
- project = attributes_for(:project, :internal, { public: true })
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)
- 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)
- end
+ project = attributes_for(:project, visibility: 'private')
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
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
@@ -612,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
@@ -625,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
@@ -636,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
@@ -682,6 +659,7 @@ describe API::Projects, api: true do
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
+ 'full_path' => user.namespace.full_path,
})
end
@@ -740,9 +718,10 @@ describe API::Projects, api: true do
get api("/projects/#{project.id}/events", current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
first_event = json_response.first
-
expect(first_event['action_name']).to eq('commented on')
expect(first_event['note']['body']).to eq('What an awesome day!')
@@ -795,11 +774,11 @@ describe API::Projects, api: true do
get api("/projects/#{project.id}/users", current_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(1)
first_user = json_response.first
-
expect(first_user['username']).to eq(member.username)
expect(first_user['name']).to eq(member.name)
expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
@@ -842,7 +821,9 @@ describe API::Projects, api: true do
it 'returns an array of project snippets' do
get api("/projects/#{project.id}/snippets", 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['title']).to eq(snippet.title)
end
@@ -864,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: '0'
+ 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
@@ -899,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
@@ -984,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
@@ -1085,52 +1068,6 @@ describe API::Projects, api: true do
end
end
- describe 'GET /projects/search/:query' do
- let!(:query) { 'query'}
- let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
- let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
- let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
- let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
- let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
- let!(:public) { create(:empty_project, :public, name: "public #{query}") }
- let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
- let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
-
- shared_examples_for 'project search response' do |args = {}|
- it 'returns project search responses' do
- get api("/projects/search/#{args[:query]}", current_user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(args[:results])
- json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project search response', query: 'query', results: 1 do
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'project search response', query: 'query', results: 6 do
- let(:current_user) { user }
- end
- it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated as a different user' do
- it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
- let(:current_user) { user2 }
- end
- end
- end
-
describe 'PUT /projects/:id' do
before { project }
before { user }
@@ -1160,7 +1097,7 @@ describe API::Projects, api: true do
end
it 'updates visibility_level' do
- project_param = { visibility_level: 20 }
+ 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|
@@ -1170,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 = { public: false }
+ 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
@@ -1243,7 +1180,7 @@ describe API::Projects, api: true do
end
it 'does not update visibility_level' do
- project_param = { visibility_level: 20 }
+ project_param = { visibility: 'public' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
@@ -1360,7 +1297,7 @@ describe API::Projects, api: true do
end
end
- describe 'DELETE /projects/:id/star' do
+ describe 'POST /projects/:id/unstar' do
context 'on a starred project' do
before do
user.toggle_star(project)
@@ -1368,16 +1305,16 @@ describe API::Projects, api: true do
end
it 'unstars the project' do
- expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
+ expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response['star_count']).to eq(0)
end
end
context 'on an unstarred project' do
it 'does not modify the star count' do
- expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+ expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count }
expect(response).to have_http_status(304)
end
@@ -1388,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
@@ -1412,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
@@ -1421,4 +1362,179 @@ describe API::Projects, api: true do
end
end
end
+
+ describe 'POST /projects/:id/fork' do
+ let(:project) do
+ create(:project, :repository, creator: user, namespace: user.namespace)
+ end
+ let(:group) { create(:group) }
+ let(:group2) do
+ group = create(:group, name: 'group2_name')
+ group.add_owner(user2)
+ group
+ end
+
+ before do
+ project.add_reporter(user2)
+ end
+
+ context 'when authenticated' do
+ it 'forks if user has sufficient access to project' do
+ post api("/projects/#{project.id}/fork", user2)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to eq(project.path)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ expect(json_response['namespace']['id']).to eq(user2.namespace.id)
+ expect(json_response['forked_from_project']['id']).to eq(project.id)
+ end
+
+ it 'forks if user is admin' do
+ post api("/projects/#{project.id}/fork", admin)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to eq(project.path)
+ expect(json_response['owner']['id']).to eq(admin.id)
+ expect(json_response['namespace']['id']).to eq(admin.namespace.id)
+ expect(json_response['forked_from_project']['id']).to eq(project.id)
+ end
+
+ it 'fails on missing project access for the project to fork' do
+ new_user = create(:user)
+ post api("/projects/#{project.id}/fork", new_user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'fails if forked project exists in the user namespace' do
+ post api("/projects/#{project.id}/fork", user)
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']['name']).to eq(['has already been taken'])
+ expect(json_response['message']['path']).to eq(['has already been taken'])
+ end
+
+ it 'fails if project to fork from does not exist' do
+ post api('/projects/424242/fork', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'forks with explicit own user namespace id' do
+ post api("/projects/#{project.id}/fork", user2), namespace: user2.namespace.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks with explicit own user name as namespace' do
+ post api("/projects/#{project.id}/fork", user2), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks to another user when admin' do
+ post api("/projects/#{project.id}/fork", admin), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'fails if trying to fork to another user when not admin' do
+ post api("/projects/#{project.id}/fork", user2), namespace: admin.namespace.id
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'fails if trying to fork to non-existent namespace' do
+ post api("/projects/#{project.id}/fork", user2), namespace: 42424242
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Target Namespace Not Found')
+ end
+
+ it 'forks to owned group' do
+ post api("/projects/#{project.id}/fork", user2), namespace: group2.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group2.name)
+ end
+
+ it 'fails to fork to not owned group' do
+ post api("/projects/#{project.id}/fork", user2), namespace: group.name
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'forks to not owned group when admin' do
+ post api("/projects/#{project.id}/fork", admin), namespace: group.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group.name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/projects/#{project.id}/fork")
+
+ expect(response).to have_http_status(401)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ 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 c61208e395c..4783d011d54 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -19,10 +19,10 @@ describe API::Repositories, api: true do
get api(route, current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
first_commit = json_response.first
-
- expect(json_response).to be_an Array
expect(first_commit['name']).to eq('bar')
expect(first_commit['type']).to eq('tree')
expect(first_commit['mode']).to eq('040000')
@@ -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
@@ -49,6 +49,7 @@ describe API::Repositories, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
+ expect(response).to include_pagination_headers
expect(json_response[4]['name']).to eq('html')
expect(json_response[4]['path']).to eq('files/html')
expect(json_response[4]['type']).to eq('tree')
@@ -65,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
@@ -99,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" }
-
- shared_examples_for 'repository blob' do
- it 'returns the repository blob' do
- get api(route, current_user)
+ describe "GET /projects/:id/repository/blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" }
- 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)
@@ -380,10 +369,10 @@ describe API::Repositories, api: true do
get api(route, current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
first_contributor = json_response.first
-
expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
expect(first_contributor['name']).to eq('tiagonbotelho')
expect(first_contributor['commits']).to eq(1)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
new file mode 100644
index 00000000000..d50fe80b36a
--- /dev/null
+++ b/spec/requests/api/runner_spec.rb
@@ -0,0 +1,1026 @@
+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
+ 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
+
+ context "when runner doesn't send version in User-Agent" do
+ let(:user_agent) { 'Go-http-client/1.1' }
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ context "when runner doesn't have a User-Agent" do
+ let(:user_agent) { nil }
+
+ it { expect(response).to have_http_status(404) }
+ 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 404 error' do
+ request_job
+
+ expect(response).to have_http_status 404
+ 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_BUILD_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_BUILD_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!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+
+ before { job.success }
+
+ 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' => job.id, 'name' => 'spinach')
+ 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_BUILD_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true },
+ { 'key' => 'CI_BUILD_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 f2d81a28cb8..8a82543a830 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -37,18 +37,20 @@ describe API::Runners, api: true do
context 'authorized user' do
it 'returns user available runners' do
get api('/runners', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
it 'filters runners by scope' do
get api('/runners?scope=active', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
@@ -73,9 +75,10 @@ describe API::Runners, api: true do
context 'with admin privileges' do
it 'returns all runners' do
get api('/runners/all', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -91,9 +94,10 @@ describe API::Runners, api: true do
it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
@@ -183,6 +187,7 @@ describe API::Runners, api: true do
it 'updates runner' do
description = shared_runner.description
active = shared_runner.active
+ runner_queue_value = shared_runner.ensure_runner_queue_value
update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active,
@@ -197,18 +202,24 @@ describe API::Runners, api: true do
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be(false)
expect(shared_runner.locked?).to be(true)
+ expect(shared_runner.ensure_runner_queue_value)
+ .not_to eq(runner_queue_value)
end
end
context 'when runner is not shared' do
it 'updates runner' do
description = specific_runner.description
+ runner_queue_value = specific_runner.ensure_runner_queue_value
+
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
expect(response).to have_http_status(200)
expect(specific_runner.description).to eq('test')
expect(specific_runner.description).not_to eq(description)
+ expect(specific_runner.ensure_runner_queue_value)
+ .not_to eq(runner_queue_value)
end
end
@@ -266,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
@@ -275,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
@@ -316,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
@@ -335,9 +350,10 @@ describe API::Runners, api: true do
context 'authorized user with master privileges' do
it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -445,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 6b9a739b439..5d75b47b3cd 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -13,6 +13,8 @@ describe API::Snippets, api: true do
get api("/snippets/", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
internal_snippet.id,
@@ -25,7 +27,10 @@ describe API::Snippets, api: true do
create(:personal_snippet, :private)
get api("/snippets/", 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(0)
end
end
@@ -43,6 +48,8 @@ describe API::Snippets, api: true do
get api("/snippets/public", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
@@ -67,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')
@@ -80,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
@@ -113,20 +120,22 @@ 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)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
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
@@ -134,16 +143,20 @@ describe API::Snippets, api: true do
end
describe 'PUT /snippets/:id' do
+ let(:visibility_level) { Snippet::PUBLIC }
let(:other_user) { create(:user) }
- let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let(:snippet) do
+ create(:personal_snippet, author: user, visibility_level: visibility_level)
+ end
+
it 'updates snippet' do
new_content = 'New content'
- put api("/snippets/#{public_snippet.id}", user), content: new_content
+ put api("/snippets/#{snippet.id}", user), content: new_content
expect(response).to have_http_status(200)
- public_snippet.reload
- expect(public_snippet.content).to eq(new_content)
+ snippet.reload
+ expect(snippet.content).to eq(new_content)
end
it 'returns 404 for invalid snippet id' do
@@ -154,7 +167,7 @@ describe API::Snippets, api: true do
end
it "returns 404 for another user's snippet" do
- put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+ put api("/snippets/#{snippet.id}", other_user), title: 'fubar'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
@@ -165,6 +178,56 @@ describe API::Snippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put api("/snippets/#{snippet.id}", user), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when a private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ 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: 'public') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'DELETE /snippets/:id' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b3e5afdadb1..d1e10f12657 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -31,6 +31,7 @@ describe API::SystemHooks, api: true do
get api("/hooks", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
expect(json_response.first['push_events']).to be true
@@ -90,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 898d2b27e5c..b132d033a61 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -20,10 +20,9 @@ describe API::Tags, api: true do
get api("/projects/#{project.id}/repository/tags", current_user)
expect(response).to have_http_status(200)
-
- first_tag = json_response.first
-
- expect(first_tag['name']).to eq(tag_name)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
end
end
@@ -43,7 +42,9 @@ describe API::Tags, api: true do
context 'without releases' do
it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", 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(tag_name)
end
@@ -59,6 +60,7 @@ describe API::Tags, api: true do
get api("/projects/#{project.id}/repository/tags", 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(tag_name)
expect(json_response.first['message']).to eq('Version 1.1.0')
@@ -135,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 d32ba60fc4c..2c83e119065 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,51 +3,53 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- shared_examples_for 'the Template Entity' do |path|
- before { get api(path) }
+ context 'the Template Entity' do
+ before { get api('/templates/gitignores/Ruby') }
it { expect(json_response['name']).to eq('Ruby') }
it { expect(json_response['content']).to include('*.gem') }
end
-
- shared_examples_for 'the TemplateList Entity' do |path|
- before { get api(path) }
+
+ context 'the TemplateList Entity' do
+ before { get api('/templates/gitignores') }
it { expect(json_response.first['name']).not_to be_nil }
it { expect(json_response.first['content']).to be_nil }
end
- shared_examples_for 'requesting gitignores' do |path|
+ context 'requesting gitignores' do
it 'returns a list of available gitignore templates' do
- get api(path)
+ get api('/templates/gitignores')
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 be > 15
end
end
- shared_examples_for 'requesting gitlab-ci-ymls' do |path|
+ context 'requesting gitlab-ci-ymls' do
it 'returns a list of available gitlab_ci_ymls' do
- get api(path)
+ get api('/templates/gitlab_ci_ymls')
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']).not_to be_nil
end
end
- shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
+ context 'requesting gitlab-ci-yml for Ruby' do
it 'adds a disclaimer on the top' do
- get api(path)
+ get api('/templates/gitlab_ci_ymls/Ruby')
expect(response).to have_http_status(200)
expect(json_response['content']).to start_with("# This file is a template,")
end
end
- shared_examples_for 'the License Template Entity' do |path|
- before { get api(path) }
+ context 'the License Template Entity' do
+ before { get api('/templates/licenses/mit') }
it 'returns a license template' do
expect(json_response['key']).to eq('mit')
@@ -56,30 +58,32 @@ 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
- shared_examples_for 'GET licenses' do |path|
+ context 'GET templates/licenses' do
it 'returns a list of available license templates' do
- get api(path)
+ get api('/templates/licenses')
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
describe 'the popular parameter' do
context 'with popular=1' do
it 'returns a list of available popular license templates' do
- get api("#{path}?popular=1")
+ get api('/templates/licenses?popular=1')
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(3)
expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
@@ -88,17 +92,17 @@ describe API::Templates, api: true do
end
end
- shared_examples_for 'GET licenses/:name' do |path|
+ context 'GET templates/licenses/:name' do
context 'with :project and :fullname given' do
before do
- get api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ get api("/templates/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
end
context 'for the mit license' 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
@@ -178,26 +182,4 @@ describe API::Templates, api: true do
end
end
end
-
- describe 'with /templates namespace' do
- it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
- it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
- it_behaves_like 'requesting gitignores', '/templates/gitignores'
- it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
- it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
- it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
- it_behaves_like 'GET licenses', '/templates/licenses'
- it_behaves_like 'GET licenses/:name', '/templates/licenses'
- end
-
- describe 'without /templates namespace' do
- it_behaves_like 'the Template Entity', '/gitignores/Ruby'
- it_behaves_like 'the TemplateList Entity', '/gitignores'
- it_behaves_like 'requesting gitignores', '/gitignores'
- it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
- it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
- it_behaves_like 'the License Template Entity', '/licenses/mit'
- it_behaves_like 'GET licenses', '/licenses'
- it_behaves_like 'GET licenses/:name', '/licenses'
- end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 56dc017ce54..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
@@ -33,6 +33,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe)
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response[0]['id']).to eq(pending_3.id)
@@ -52,6 +53,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { author_id: author_2.id }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -64,6 +66,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { type: 'MergeRequest' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -74,6 +77,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { state: 'done' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -84,6 +88,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { project_id: project_2.id }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -94,6 +99,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { action: 'mentioned' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -101,46 +107,47 @@ describe API::Todos, api: true do
end
end
- describe 'DELETE /todos/:id' do
+ describe 'POST /todos/:id/mark_as_done' do
context 'when unauthenticated' do
it 'returns authentication error' do
- delete api("/todos/#{pending_1.id}")
+ post api("/todos/#{pending_1.id}/mark_as_done")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'marks a todo as done' do
- delete api("/todos/#{pending_1.id}", john_doe)
+ post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(pending_1.id)
+ expect(json_response['state']).to eq('done')
expect(pending_1.reload).to be_done
end
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
- delete api("/todos/#{pending_1.id}", john_doe)
+ post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
end
end
end
- describe 'DELETE /todos' do
+ describe 'POST /mark_as_done' do
context 'when unauthenticated' do
it 'returns authentication error' do
- delete api('/todos')
+ post api('/todos/mark_as_done')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'marks all todos as done' do
- delete api('/todos', john_doe)
+ post api('/todos/mark_as_done', john_doe)
- expect(response.status).to eq(200)
- expect(response.body).to eq('3')
+ expect(response).to have_http_status(204)
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
@@ -149,14 +156,14 @@ describe API::Todos, api: true do
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
- delete api("/todos", john_doe)
+ post api("/todos/mark_as_done", john_doe)
end
end
end
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
@@ -173,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
@@ -188,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 84104aa66ee..424c02932ab 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,9 +49,11 @@ 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)
@@ -56,15 +61,17 @@ describe API::Triggers do
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' }
+ 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
- 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')
+ 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')
+
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 +80,24 @@ 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)
- pipeline.builds.reload
- expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+ expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables)
end
end
end
@@ -100,6 +109,7 @@ describe API::Triggers do
get 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
@@ -122,17 +132,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
@@ -140,7 +150,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
@@ -148,7 +158,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
@@ -157,19 +167,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
@@ -177,24 +199,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 '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/:token' do
+ 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
@@ -202,7 +287,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
@@ -210,7 +295,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 5bf5bf0739e..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
@@ -40,7 +42,9 @@ describe API::Users, api: true do
it "returns an array of users" do
get api("/users", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
@@ -55,13 +59,16 @@ describe API::Users, api: true do
get api("/users?blocked=true", 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 all(include('state' => /(blocked|ldap_blocked)/))
end
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", 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['username']).to eq(omniauth_user.username)
end
@@ -70,7 +77,9 @@ describe API::Users, api: true do
context "when admin" do
it "returns an array of users" do
get api("/users", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
expect(json_response.first.keys).to include 'organization'
@@ -87,6 +96,7 @@ describe API::Users, api: true do
get api("/users?external=true", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(include('external' => true))
end
@@ -190,6 +200,18 @@ describe API::Users, api: true do
expect(new_user.external).to be_truthy
end
+ it "creates user with reset password" do
+ post api('/users', admin), attributes_for(:user, reset_password: true).except(:password)
+
+ expect(response).to have_http_status(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+
+ expect(new_user).not_to eq(nil)
+ expect(new_user.recently_sent_password_reset?).to eq(true)
+ end
+
it "does not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
@@ -305,6 +327,13 @@ describe API::Users, api: true do
expect(user.reload.bio).to eq('new test bio')
end
+ it "updates user with new password and forces reset on next login" do
+ put api("/users/#{user.id}", admin), password: '12345678'
+
+ expect(response).to have_http_status(200)
+ expect(user.reload.password_expires_at).to be <= Time.now
+ end
+
it "updates user with organization" do
put api("/users/#{user.id}", admin), { organization: 'GitLab' }
@@ -488,8 +517,11 @@ describe API::Users, api: true do
it 'returns array of ssh keys' do
user.keys << key
user.save
+
get api("/users/#{user.id}/keys", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
@@ -510,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
@@ -576,8 +610,11 @@ describe API::Users, api: true do
it 'returns array of emails' do
user.emails << email
user.save
+
get api("/users/#{user.id}/emails", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
@@ -604,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
@@ -638,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
@@ -691,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
@@ -710,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
@@ -720,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
@@ -728,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
@@ -755,8 +794,11 @@ describe API::Users, api: true do
it "returns array of ssh keys" do
user.keys << key
user.save
+
get api("/user/keys", 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["title"]).to eq(key.title)
end
@@ -833,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
@@ -872,8 +916,11 @@ describe API::Users, api: true do
it "returns array of emails" do
user.emails << email
user.save
+
get api("/user/emails", 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["email"]).to eq(email.email)
end
@@ -937,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
@@ -964,69 +1013,69 @@ describe API::Users, api: true do
end
end
- describe 'PUT /users/:id/block' do
+ describe 'POST /users/:id/block' do
before { admin }
it 'blocks existing user' do
- put api("/users/#{user.id}/block", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{user.id}/block", admin)
+ expect(response).to have_http_status(201)
expect(user.reload.state).to eq('blocked')
end
it 'does not re-block ldap blocked users' do
- put api("/users/#{ldap_blocked_user.id}/block", admin)
+ post api("/users/#{ldap_blocked_user.id}/block", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
- put api("/users/#{user.id}/block", user)
+ post api("/users/#{user.id}/block", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
- put api('/users/9999/block', admin)
+ post api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
- describe 'PUT /users/:id/unblock' do
+ describe 'POST /users/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
it 'unblocks existing user' do
- put api("/users/#{user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{user.id}/unblock", admin)
+ expect(response).to have_http_status(201)
expect(user.reload.state).to eq('active')
end
it 'unblocks a blocked user' do
- put api("/users/#{blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(201)
expect(blocked_user.reload.state).to eq('active')
end
it 'does not unblock ldap blocked users' do
- put api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ post api("/users/#{ldap_blocked_user.id}/unblock", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
- put api("/users/#{user.id}/unblock", user)
+ post api("/users/#{user.id}/unblock", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
- put api('/users/9999/block', admin)
+ post api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
- put api("/users/ASDF/block", admin)
+ post api("/users/ASDF/block", admin)
expect(response).to have_http_status(404)
end
@@ -1054,14 +1103,14 @@ describe API::Users, api: true do
end
context "as a user than can see the event's project" do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/users/#{user.id}/events", user) }
- end
-
context 'joined event' do
it 'returns the "joined" event' do
get 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)
@@ -1108,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
new file mode 100644
index 00000000000..eb95934f354
--- /dev/null
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe API::V3::Boards, api: true do
+ include ApiHelpers
+
+ 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
+ create(:label, title: 'Development', color: '#FFAABB', project: project)
+ end
+
+ let!(:test_label) do
+ create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ end
+
+ let!(:dev_list) do
+ create(:list, label: dev_label, position: 1)
+ end
+
+ let!(:test_list) do
+ create(:list, label: test_label, position: 2)
+ end
+
+ let!(:board) do
+ create(:board, project: project, lists: [dev_list, test_list])
+ end
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /projects/:id/boards" do
+ let(:base_url) { "/projects/#{project.id}/boards" }
+
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api(base_url)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the project issue board" do
+ get v3_api(base_url, 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(board.id)
+ expect(json_response.first['lists']).to be_an Array
+ expect(json_response.first['lists'].length).to eq(2)
+ expect(json_response.first['lists'].last).to have_key('position')
+ end
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns issue board lists' do
+ get v3_api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get v3_api("/projects/#{project.id}/boards/22343/lists", user)
+
+ 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
new file mode 100644
index 00000000000..e4cedf98e64
--- /dev/null
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+require 'mime/types'
+
+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_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
+ project.repository.expire_all_method_caches
+
+ get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
+
+ expect(response).to have_http_status(200)
+ 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
+ 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
+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
new file mode 100644
index 00000000000..e298ef055e1
--- /dev/null
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -0,0 +1,578 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Commits, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
+ let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
+
+ before { project.team << [user, :reporter] }
+
+ describe "List repository commits" do
+ context "authorized user" do
+ before { project.team << [user2, :reporter] }
+
+ it "returns project commits" do
+ commit = project.repository.commit
+ get v3_api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['committer_name']).to eq(commit.committer_name)
+ expect(json_response.first['committer_email']).to eq(commit.committer_email)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return project commits" do
+ get v3_api("/projects/#{project.id}/repository/commits")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "since optional parameter" do
+ it "returns project commits since provided parameter" do
+ commits = project.repository.commits("master")
+ since = commits.second.created_at
+
+ get v3_api("/projects/#{project.id}/repository/commits?since=#{since.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
+ end
+
+ context "until optional parameter" do
+ it "returns project commits until provided parameter" do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
+
+ get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+
+ if commits.size >= 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
+
+ expect(json_response.first["id"]).to eq(commits.second.id)
+ expect(json_response.second["id"]).to eq(commits.third.id)
+ end
+ end
+
+ context "invalid xmlschema date parameters" do
+ it "returns an invalid parameter error message" do
+ get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('since is invalid')
+ end
+ end
+
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
+
+ get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ end
+ end
+ end
+
+ describe "Create a commit with multiple files and actions" do
+ let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+ it 'returns a 403 unauthorized for user without permissions' do
+ post v3_api(url, user2)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns a 400 bad request if no params are given' do
+ post v3_api(url, user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context :create do
+ let(:message) { 'Created file' }
+ let!(:invalid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'a new file in project repo' do
+ post v3_api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['committer_email']).to eq(user.email)
+ end
+
+ it 'returns a 400 bad request if file exists' do
+ post v3_api(url, user), invalid_c_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'with project path in URL' do
+ let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
+
+ it 'a new file in project repo' do
+ post v3_api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ context :delete do
+ let(:message) { 'Deleted file' }
+ let!(:invalid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ }
+ ]
+ }
+ end
+ let!(:valid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/users.md'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_d_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_d_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :move do
+ let(:message) { 'Moved file' }
+ let!(:invalid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+ let!(:valid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_m_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_m_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :update do
+ let(:message) { 'Updated file' }
+ let!(:invalid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_u_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_u_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context "multiple operations" do
+ let(:message) { 'Multiple actions' }
+ let!(:invalid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'doc/v3_api/projects.md'
+ },
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'Gemfile.zip'
+ },
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'are commited as one in project repo' do
+ post v3_api(url, user), valid_mo_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'return a 400 bad request if there are any issues' do
+ post v3_api(url, user), invalid_mo_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "Get a single commit" do
+ context "authorized user" do
+ it "returns a commit by sha" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.repository.commit.id)
+ expect(json_response['title']).to eq(project.repository.commit.title)
+ expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
+ expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
+ expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
+ end
+
+ it "returns a 404 error if not found" do
+ get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns nil for commit without CI" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to be_nil
+ end
+
+ it "returns status for CI" do
+ pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
+ pipeline.update(status: 'success')
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq(pipeline.status)
+ end
+
+ it "returns status for CI when pipeline is created" do
+ project.ensure_pipeline('master', project.repository.commit.sha)
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq("created")
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe "Get the diff of a commit" do
+ context "authorized user" do
+ before { project.team << [user2, :reporter] }
+
+ it "returns the diff of the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
+ expect(response).to have_http_status(200)
+
+ expect(json_response).to be_an Array
+ expect(json_response.length).to be >= 1
+ expect(json_response.first.keys).to include "diff"
+ end
+
+ it "returns a 404 error if invalid commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return the diff of the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'Get the comments of a commit' do
+ context 'authorized user' do
+ it 'returns merge_request comments' do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['note']).to eq('a comment on a commit')
+ expect(json_response.first['author']['id']).to eq(user.id)
+ end
+
+ it 'returns a 404 error if merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return the diff of the selected commit' do
+ get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+
+ context 'authorized user' do
+ it 'cherry picks a commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(master_pickable_commit.title)
+ expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+
+ it 'returns 400 if commit is already included in the target branch' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
+ A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ project.team << [user2, :developer]
+ protected_branch = create(:protected_branch, project: project, name: 'feature')
+
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+
+ it 'returns 404 if commit is not found' do
+ post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Commit Not Found')
+ end
+
+ it 'returns 404 if branch is not found' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Branch Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not cherry pick the commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'Post comment to commit' do
+ context 'authorized user' do
+ it 'returns comment' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['path']).to be_nil
+ expect(json_response['line']).to be_nil
+ expect(json_response['line_type']).to be_nil
+ end
+
+ it 'returns the inline comment' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
+ expect(json_response['line']).to eq(1)
+ expect(json_response['line_type']).to eq('new')
+ end
+
+ it 'returns 400 if note is missing' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 404 if note is attached to non existent commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return the diff of the selected commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
new file mode 100644
index 00000000000..f5bdf408c5e
--- /dev/null
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe API::V3::DeployKeys, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ describe 'GET /deploy_keys' do
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get v3_api('/deploy_keys')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 403 error' do
+ get v3_api('/deploy_keys', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'should return all deploy keys' do
+ get v3_api('/deploy_keys', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+ end
+ end
+ end
+
+ %w(deploy_keys keys).each do |path|
+ describe "GET /projects/:id/#{path}" do
+ before { deploy_key }
+
+ it 'should return array of ssh keys' do
+ get v3_api("/projects/#{project.id}/#{path}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(deploy_key.title)
+ end
+ end
+
+ describe "GET /projects/:id/#{path}/:key_id" do
+ it 'should return a single key' do
+ get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(deploy_key.title)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ get v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/deploy_keys" do
+ it 'should not create an invalid ssh key' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'should not create a key without title' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('title is missing')
+ end
+
+ it 'should create new ssh key' do
+ key_attrs = attributes_for :another_key
+
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
+ end.to change{ project.deploy_keys.count }.by(1)
+ end
+
+ it 'returns an existing ssh key when attempting to add a duplicate' do
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.not_to change { project.deploy_keys.count }
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'joins an existing ssh key to a new project' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.to change { project2.deploy_keys.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ describe "DELETE /projects/:id/#{path}/:key_id" do
+ before { deploy_key }
+
+ it 'should delete existing key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+ end.to change{ project.deploy_keys.count }.by(-1)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ delete v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/#{path}/:key_id/enable" do
+ let(:project2) { create(:empty_project) }
+
+ context 'when the user can admin the project' do
+ it 'enables the key' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
+ end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
+ context 'when the user can admin the project' do
+ it 'disables the key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
+ end.to change { project.deploy_keys.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 00000000000..3b61139a2cd
--- /dev/null
+++ b/spec/requests/api/v3/files_spec.rb
@@ -0,0 +1,285 @@
+require 'spec_helper'
+
+describe API::V3::Files, api: true do
+ include ApiHelpers
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # 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] }
+
+ describe "GET /projects/:id/repository/files" do
+ let(:route) { "/projects/#{project.id}/repository/files" }
+
+ shared_examples_for 'repository files' do
+ it "returns file info" do
+ get v3_api(route, current_user), params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(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_behaves_like '400 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+
+ context 'when file_path does not exist' do
+ let(:params) do
+ {
+ file_path: 'app/models/application.rb',
+ ref: 'master',
+ }
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route, 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 v3_api(route, current_user), params }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository 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 v3_api(route), params }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository files' 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), params }
+ end
+ end
+ end
+
+ describe "POST /projects/:id/repository/files" do
+ let(:valid_params) do
+ {
+ file_path: 'newfile.rb',
+ branch_name: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile'
+ }
+ end
+
+ 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
+
+ it "returns a 400 bad request if no params given" do
+ post v3_api("/projects/#{project.id}/repository/files", 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(:create_file).
+ and_return(false)
+
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" do
+ it "creates a new file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ 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
+ let(:valid_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ content: 'puts 8',
+ commit_message: 'Changed file'
+ }
+ end
+
+ it "updates existing file in project repo" do
+ put v3_api("/projects/#{project.id}/repository/files", 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)
+ end
+
+ it "returns a 400 bad request if no params given" do
+ put v3_api("/projects/#{project.id}/repository/files", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" 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 v3_api("/projects/#{project.id}/repository/files", 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)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/repository/files" do
+ let(:valid_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ commit_message: 'Changed file'
+ }
+ end
+
+ it "deletes existing file in project repo" do
+ delete v3_api("/projects/#{project.id}/repository/files", 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)
+ end
+
+ it "returns a 400 bad request if no params given" do
+ delete v3_api("/projects/#{project.id}/repository/files", 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(:delete_file).and_return(false)
+
+ delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" do
+ it "removes a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ delete v3_api("/projects/#{project.id}/repository/files", 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)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/repository/files with binary file" do
+ let(:file_path) { 'test.bin' }
+ let(:put_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
+ commit_message: 'Binary file with a \n should not be touched',
+ encoding: 'base64'
+ }
+ end
+ let(:get_params) do
+ {
+ file_path: file_path,
+ ref: 'master',
+ }
+ end
+
+ before do
+ post v3_api("/projects/#{project.id}/repository/files", user), put_params
+ end
+
+ it "remains unchanged" do
+ get v3_api("/projects/#{project.id}/repository/files", 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['content']).to eq(put_params[:content])
+ end
+ end
+end
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
new file mode 100644
index 00000000000..1941ca0d7d8
--- /dev/null
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -0,0 +1,1293 @@
+require 'spec_helper'
+
+describe API::V3::Issues, api: true do
+ include ApiHelpers
+ include EmailHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 3.hours.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee,
+ created_at: generate(:issue_created_at),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: project,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 1.hour.ago
+ end
+ let!(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let!(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { URI.escape(Milestone::None.title) }
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /issues" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/issues")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of issues" do
+ get v3_api("/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'returns an array of closed issues' do
+ get v3_api('/issues?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_issue.id)
+ end
+
+ it 'returns an array of opened issues' do
+ get v3_api('/issues?state=opened', 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(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get v3_api('/issues?state=all', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of labeled issues' do
+ get v3_api("/issues?labels=#{label.title}", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues when at least one label matches' do
+ get v3_api("/issues?labels=#{label.title},foo,bar", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get v3_api('/issues?labels=foo,bar', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled issues matching given state' do
+ get v3_api("/issues?labels=#{label.title}&state=opened", 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['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an empty array if no issue matches labels and state filters' do
+ get v3_api("/issues?labels=#{label.title}&state=closed", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ 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_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("/issues?milestone=#{no_milestone_title}", author)
+
+ 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(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api('/issues', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api('/issues?sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api('/issues?order_by=updated_at', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api('/issues?order_by=updated_at&sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ 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
+
+ describe "GET /groups/:id/issues" do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignee: assignee,
+ updated_at: 2.hours.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ before do
+ group_project.team << [user, :reporter]
+ end
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ it 'returns group issues without confidential issues for non project members' do
+ get v3_api(base_url, non_member)
+
+ 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['title']).to eq(group_issue.title)
+ end
+
+ it 'returns group confidential issues for author' do
+ get v3_api(base_url, author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get v3_api(base_url, assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get v3_api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for admin' do
+ get v3_api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns an array of labeled group issues' do
+ get v3_api("#{base_url}?labels=#{group_label.title}", 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['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ 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 v3_api("#{base_url}?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", 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(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
+ '&state=closed'
+
+ 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(group_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}?milestone=#{no_milestone_title}", 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(group_confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api(base_url, user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ it "returns 404 on private projects for other users" do
+ private_project = create(:empty_project, :private)
+ create(:issue, project: private_project)
+
+ get v3_api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ create(:issue, project: restricted_project)
+
+ get v3_api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect(json_response).to eq([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get v3_api("#{base_url}/issues", non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get v3_api("#{base_url}/issues", guest)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for author' do
+ get v3_api("#{base_url}/issues", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get v3_api("#{base_url}/issues", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get v3_api("#{base_url}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for admin' do
+ get v3_api("#{base_url}/issues", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns an array of labeled project issues' do
+ get v3_api("#{base_url}/issues?labels=#{label.title}", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled project issues where all labels match' do
+ get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get v3_api("#{base_url}/issues?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ 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_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", 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(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api("#{base_url}/issues", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}/issues?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues/:issue_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(issue.id)
+ expect(json_response['iid']).to eq(issue.iid)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['description']).to eq(issue.description)
+ expect(json_response['state']).to eq(issue.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(issue.label_names)
+ expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it "returns a project issue by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns a project issue by iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 1
+ expect(json_response.first['title']).to eq issue.title
+ expect(json_response.first['id']).to eq issue.id
+ expect(json_response.first['iid']).to eq issue.iid
+ end
+
+ it 'returns an empty array for an unknown project issue iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 0
+ end
+
+ it "returns 404 if issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/54321", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it "returns 404 for non project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 for project members with guest role" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns confidential issue for project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/issues" do
+ it 'creates a new project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2'
+
+ 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(%w(label label2))
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a new confidential project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'y'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: false
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it "returns a 400 bad request if title not given" do
+ post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving issues in a merge request' 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 v3_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
+
+ 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
+
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ 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['due_date']).to eq(due_date)
+ end
+ 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", user),
+ title: 'new issue', labels: 'label, label2', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2'
+ end.not_to change { project.labels.count }
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" do
+ expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update only title" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", 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
+ put v3_api("/projects/#{project.id}/issues/44444", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'confidential issues' do
+ it "returns 403 for non project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "updates a confidential issue for project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ confidential: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('updated title')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ it "sends notifications for subscribers of newly added labels when issue is updated" do
+ label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it 'removes all labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'updates labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'foo,bar'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", 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'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update state and label" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label2', state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['state']).to eq "closed"
+ end
+
+ it 'reopens a project isssue' do
+ put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'reopened'
+ end
+
+ 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label3', state_event: 'close', updated_at: update_time
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", 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
+ it "rejects a non member from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a developer from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the issue if an admin requests it" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete v3_api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_id/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 v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project.id)
+ end
+
+ context 'when source and target projects are the same' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: project.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
+ end
+ end
+
+ context 'when the user does not have the permission to move issues' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
+ end
+ end
+
+ it 'moves the issue to another namespace if I am admin' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/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 issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/123/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 source project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/123/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 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: 123
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/subscription' do
+ it 'subscribes to an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE :id/issues/:issue_id/subscription' do
+ it 'unsubscribes from an issue' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { 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
new file mode 100644
index 00000000000..dfac357d37c
--- /dev/null
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+
+describe API::V3::Labels, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let!(:label1) { create(:label, title: 'label1', project: project) }
+ let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/labels' do
+ it 'returns all available labels to the project' do
+ group = create(:group)
+ group_label = create(:group_label, title: 'feature', group: group)
+ project.update(group: group)
+ create(:labeled_issue, project: project, labels: [group_label], author: user)
+ 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 = %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)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.first.keys).to match_array expected_keys
+ expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
+
+ label1_response = json_response.find { |l| l['name'] == label1.title }
+ group_label_response = json_response.find { |l| l['name'] == group_label.title }
+ priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+ expect(label1_response['open_issues_count']).to eq(0)
+ expect(label1_response['closed_issues_count']).to eq(1)
+ expect(label1_response['open_merge_requests_count']).to eq(0)
+ expect(label1_response['name']).to eq(label1.name)
+ expect(label1_response['color']).to be_present
+ expect(label1_response['description']).to be_nil
+ expect(label1_response['priority']).to be_nil
+ expect(label1_response['subscribed']).to be_falsey
+
+ expect(group_label_response['open_issues_count']).to eq(1)
+ expect(group_label_response['closed_issues_count']).to eq(0)
+ expect(group_label_response['open_merge_requests_count']).to eq(0)
+ expect(group_label_response['name']).to eq(group_label.name)
+ expect(group_label_response['color']).to be_present
+ expect(group_label_response['description']).to be_nil
+ expect(group_label_response['priority']).to be_nil
+ expect(group_label_response['subscribed']).to be_falsey
+
+ expect(priority_label_response['open_issues_count']).to eq(0)
+ expect(priority_label_response['closed_issues_count']).to eq(0)
+ expect(priority_label_response['open_merge_requests_count']).to eq(1)
+ expect(priority_label_response['name']).to eq(priority_label.name)
+ expect(priority_label_response['color']).to be_present
+ expect(priority_label_response['description']).to be_nil
+ expect(priority_label_response['priority']).to eq(3)
+ expect(priority_label_response['subscribed']).to be_falsey
+ end
+ end
+
+ describe "POST /projects/:id/labels/:label_id/subscription" do
+ context "when label_id is a label title" do
+ it "subscribes to the label" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "subscribes to the label" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when user is already subscribed to label" do
+ before { label1.subscribe(user, project) }
+
+ it "returns 304" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "returns 404 error" do
+ post v3_api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/labels/:label_id/subscription" do
+ before { label1.subscribe(user, project) }
+
+ context "when label_id is a label title" do
+ it "unsubscribes from the label" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "unsubscribes from the label" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when user is already unsubscribed from label" do
+ before { label1.unsubscribe(user, project) }
+
+ it "returns 304" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "returns 404 error" do
+ delete v3_api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ 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
new file mode 100644
index 00000000000..13814ed10c3
--- /dev/null
+++ b/spec/requests/api/v3/members_spec.rb
@@ -0,0 +1,342 @@
+require 'spec_helper'
+
+describe API::V3::Members, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
+ end
+
+ let!(:group) do
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+ end
+ end
+
+ it 'does not return invitees' do
+ create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
+
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+
+ it 'finds members with query string' do
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+ expect(response).to have_http_status(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['username']).to eq(master.username)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(200)
+ # User attributes
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['name']).to eq(developer.name)
+ expect(json_response['username']).to eq(developer.username)
+ expect(json_response['state']).to eq(developer.state)
+ expect(json_response['avatar_url']).to eq(developer.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+ user_id: access_requester.id, access_level: Member::MASTER
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", user),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and new member is already a requester' do
+ it 'transforms the requester into a proper member' do
+ expect do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(source.requesters.count).to eq(0)
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+ end
+
+ it 'creates a new member' do
+ expect do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: master.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ end
+
+ it 'returns 400 when user_id is not given' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access_level is not valid' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+ access_level: Member::MASTER
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: Member::MASTER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it 'returns 409 if member does not exist' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access level is not valid' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a member and deleting themself' do
+ it 'deletes the member' do
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+ expect(response).to have_http_status(200)
+ 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
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ it 'deletes the member' do
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ context 'Adding owner to project' do
+ it 'returns 403' do
+ expect do
+ post v3_api("/projects/#{project.id}/members", master),
+ user_id: stranger.id, access_level: Member::OWNER
+
+ expect(response).to have_http_status(422)
+ end.to change { project.members.count }.by(0)
+ end
+ end
+end
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
new file mode 100644
index 00000000000..d73e9635c9b
--- /dev/null
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -0,0 +1,733 @@
+require "spec_helper"
+
+describe API::MergeRequests, api: true do
+ include ApiHelpers
+ let(:base_time) { Time.now }
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:non_member) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+
+ before do
+ project.team << [user, :reporter]
+ end
+
+ describe "GET /projects/:id/merge_requests" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/projects/#{project.id}/merge_requests")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of open merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=opened", 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.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of closed merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?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['title']).to eq(merge_request_closed.title)
+ end
+
+ it "returns an array of merged merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=merged", 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['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
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it "returns an array of merge_requests in ascending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it "returns an array of merge_requests in descending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by updated_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+ end
+
+ describe "GET /projects/:id/merge_requests/:merge_request_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(merge_request.id)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['project_id']).to eq(merge_request.project.id)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['description']).to eq(merge_request.description)
+ expect(json_response['state']).to eq(merge_request.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(merge_request.label_names)
+ expect(json_response['milestone']).to be_nil
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_branch']).to eq(merge_request.target_branch)
+ expect(json_response['source_branch']).to eq(merge_request.source_branch)
+ expect(json_response['upvotes']).to eq(0)
+ expect(json_response['downvotes']).to eq(0)
+ 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_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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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)
+ expect(json_response['work_in_progress']).to eq(false)
+ 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 by iid' do
+ url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
+ get v3_api(url, user)
+ expect(response.status).to eq 200
+ expect(json_response.first['title']).to eq merge_request.title
+ expect(json_response.first['id']).to eq merge_request.id
+ end
+
+ it 'returns merge_request by iid array' do
+ get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.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.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", 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
+ it 'returns a 200 when merge request is valid' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+ commit = merge_request.commits.first
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(merge_request.commits.size)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['title']).to eq(commit.title)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+ it 'returns the change information of the merge_request' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/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
+ get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests" do
+ context 'between branches projects' do
+ it "returns merge_request" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label2',
+ milestone_id: milestone.id,
+ remove_source_branch: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ 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
+
+ it "returns 422 when source_branch equals target_branch" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
+ expect(response).to have_http_status(422)
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", target_branch: "master", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "markdown", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ target_branch: 'master', source_branch: 'markdown'
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'markdown',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'with existing MR' do
+ before do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ @mr = MergeRequest.all.last
+ end
+
+ it 'returns 409 when MR already exists for source/target' do
+ expect do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'New test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ end.to change { MergeRequest.count }.by(0)
+ expect(response).to have_http_status(409)
+ end
+ end
+ end
+
+ context 'forked projects' do
+ let!(:user2) { create(:user) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
+
+ before :each do |each|
+ fork_project.team << [user2, :reporter]
+ end
+
+ it "returns merge_request" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
+ author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['description']).to eq('Test description for Test merge_request')
+ end
+
+ it "does not return 422 when source_branch equals target_branch" do
+ expect(project.id).not_to eq(fork_project.id)
+ expect(fork_project.forked?).to be_truthy
+ expect(fork_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when target_branch is specified' do
+ it 'returns 422 if not a forked project' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user,
+ target_project_id: fork_project.id
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns 422 if targeting a different fork' do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: unrelated_project.id
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ it "returns 201 when target_branch is specified and for the same project" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ context "when the user is developer" do
+ let(:developer) { create(:user) }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ it "denies the deletion of the merge request" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
+
+ it "returns merge_request in case of success" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 406 if branch can't be merged" do
+ allow_any_instance_of(MergeRequest).
+ to receive(:can_be_merged?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(406)
+ expect(json_response['message']).to eq('Branch cannot be merged')
+ end
+
+ it "returns 405 if merge_request is not open" do
+ merge_request.close
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 401 if user has no permissions to merge" do
+ user2 = create(:user)
+ project.team << [user2, :reporter]
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "enables merge when pipeline succeeds if the pipeline is active" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_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)
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ context "to close a MR" do
+ it "returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ end
+ end
+
+ it "updates title and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'does not update state when title is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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
+ end
+
+ describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+ it "returns comment" do
+ original_count = merge_request.notes.size
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['author']['name']).to eq(user.name)
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(merge_request.reload.notes.size).to eq(original_count + 1)
+ end
+
+ it "returns 400 if note is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 404 if note is attached to non existent merge request" do
+ post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
+ note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET :id/merge_requests/:merge_request_id/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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['note']).to eq("a comment on a MR")
+ expect(json_response.first['author']['id']).to eq(user.id)
+ 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
+ get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET :id/merge_requests/:merge_request_id/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 v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", 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(issue.id)
+ end
+
+ it 'returns an empty array when there are no issues to be closed' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ 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 v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", 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['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ it 'subscribes to a merge request' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ it 'unsubscribes from a merge request' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", 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]
+
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'V3 time tracking endpoints', 'merge_request'
+ end
+
+ def mr_with_later_created_and_updated_at_time
+ merge_request
+ merge_request.created_at += 1.hour
+ merge_request.updated_at += 30.minutes
+ merge_request.save
+ merge_request
+ end
+
+ def mr_with_earlier_created_and_updated_at_time
+ merge_request_closed
+ merge_request_closed.created_at -= 1.hour
+ merge_request_closed.updated_at -= 30.minutes
+ merge_request_closed.save
+ merge_request_closed
+ end
+end
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/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
new file mode 100644
index 00000000000..957a3bf97ef
--- /dev/null
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -0,0 +1,228 @@
+require 'rails_helper'
+
+describe API::ProjectSnippets, api: true do
+ include ApiHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ snippet = create(:project_snippet, author: admin)
+
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/' do
+ let(:user) { create(:user) }
+
+ it 'returns all snippets available to team member' do
+ project.add_developer(user)
+ public_snippet = create(:project_snippet, :public, project: project)
+ internal_snippet = create(:project_snippet, :internal, project: project)
+ private_snippet = create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(3)
+ expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'POST /projects/:project_id/snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ code: 'puts "hello world"',
+ visibility_level: Snippet::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(201)
+ snippet = ProjectSnippet.find(json_response['id'])
+ 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])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post v3_api("/projects/#{project.id}/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(project, 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(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'PUT /projects/:project_id/snippets/:id/' do
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) }
+
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
+
+ expect(response).to have_http_status(200)
+ snippet.reload
+ expect(snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
+
+ 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("/projects/#{project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'creates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'deletes snippet' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/:id/raw' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'returns raw text' do
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
+
+ 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("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ 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/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
new file mode 100644
index 00000000000..d8bb562587d
--- /dev/null
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -0,0 +1,1452 @@
+require 'spec_helper'
+
+describe API::V3::Projects, api: true do
+ include ApiHelpers
+ include Gitlab::CurrentSettings
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
+ let(:project_member) { create(:project_member, :master, user: user, project: project) }
+ let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
+ let(:user4) { create(:user) }
+ let(:project3) do
+ create(:project,
+ :private,
+ :repository,
+ name: 'second_project',
+ path: 'second_project',
+ creator_id: user.id,
+ namespace: user.namespace,
+ merge_requests_enabled: false,
+ issues_enabled: false, wiki_enabled: false,
+ snippets_enabled: false)
+ end
+ let(:project_member3) do
+ create(:project_member,
+ user: user4,
+ project: project3,
+ access_level: ProjectMember::MASTER)
+ end
+ let(:project4) do
+ create(:empty_project,
+ name: 'third_project',
+ path: 'third_project',
+ creator_id: user4.id,
+ namespace: user4.namespace)
+ end
+
+ describe 'GET /projects' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns an array of projects' do
+ get v3_api('/projects', user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project.name)
+ expect(json_response.first['owner']['username']).to eq(user.username)
+ end
+
+ it 'includes the project labels as the tag_list' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('tag_list')
+ end
+
+ it 'includes open_issues_count' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('open_issues_count')
+ end
+
+ it 'does not include open_issues_count' do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).not_to include('open_issues_count')
+ end
+
+ context 'GET /projects?simple=true' do
+ it 'returns a simplified version of all the projects' do
+ 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)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to match_array expected_keys
+ end
+ end
+
+ context 'and using search' do
+ it 'returns searched project' do
+ get v3_api('/projects', user), { search: project.name }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the visibility filter' do
+ it 'filters based on private visibility param' do
+ get v3_api('/projects', user), { visibility: 'private' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
+ end
+
+ it 'filters based on internal visibility param' do
+ get v3_api('/projects', user), { visibility: 'internal' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
+ end
+
+ it 'filters based on public visibility param' do
+ get v3_api('/projects', user), { visibility: 'public' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
+ end
+ end
+
+ context 'and using sorting' do
+ before do
+ project2
+ project3
+ end
+
+ it 'returns the correct order when sorted by id' do
+ get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(project3.id)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/all' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all', user)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns an array of all projects' do
+ get v3_api('/projects/all', admin)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ expect(json_response).to satisfy do |response|
+ response.one? do |entry|
+ entry.has_key?('permissions') &&
+ entry['name'] == project.name &&
+ entry['owner']['username'] == user.username
+ end
+ end
+ end
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/all', 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
+ get v3_api('/projects/all', admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include 'statistics'
+ end
+ end
+ end
+
+ describe 'GET /projects/owned' do
+ before do
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'returns an array of projects the user owns' do
+ get v3_api('/projects/owned', user4)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project4.name)
+ expect(json_response.first['owner']['username']).to eq(user4.username)
+ end
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/owned', user4)
+
+ 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 = {
+ commit_count: 23,
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }
+
+ project4.statistics.update!(attributes)
+
+ get v3_api('/projects/owned', user4), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ end
+ end
+ end
+
+ describe 'GET /projects/visible' do
+ shared_examples_for 'visible projects response' do
+ it 'returns the visible projects' do
+ get v3_api('/projects/visible', current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
+ end
+ end
+
+ let!(:public_project) { create(:empty_project, :public) }
+ before do
+ project
+ project2
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user2 }
+ let(:projects) { [public_project] }
+ end
+ end
+ end
+
+ describe 'GET /projects/starred' do
+ let(:public_project) { create(:empty_project, :public) }
+
+ before do
+ project_member2
+ user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ end
+
+ it 'returns the starred projects viewable by the user' do
+ get v3_api('/projects/starred', user3)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
+ end
+ end
+
+ describe 'POST /projects' do
+ context 'maximum number of projects reached' do
+ it 'does not create new project and respond with 403' do
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
+ expect { post v3_api('/projects', user2), name: 'foo' }.
+ to change {Project.count}.by(0)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ 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
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
+ post v3_api('/projects', user2), name: 'foo'
+ expect(response).to have_http_status(201)
+ end
+
+ 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
+
+ it "assigns attributes to project" do
+ project = attributes_for(:project, {
+ path: 'camelCasePath',
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false,
+ request_access_enabled: true,
+ only_allow_merge_if_all_discussions_are_resolved: false
+ })
+
+ post v3_api('/projects', user), project
+
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+
+ # Check feature permissions attributes
+ project = Project.find_by_path(project[:path])
+ expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 })
+ post v3_api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ 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
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if all discussions are resolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+
+ context 'when a visibility level is restricted' do
+ before do
+ @project = attributes_for(:project, { public: true })
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it 'does not allow a non-admin to use a restricted visibility level' do
+ post v3_api('/projects', user), @project
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['visibility_level'].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
+
+ it 'allows an admin to override restricted visibility settings' do
+ post v3_api('/projects', admin), @project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to(
+ eq(Gitlab::VisibilityLevel::PUBLIC)
+ )
+ end
+ end
+ end
+
+ describe 'POST /projects/user/:id' do
+ before { project }
+ before { admin }
+
+ it 'should create new project without path and return 201' do
+ expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
+ expect(response).to have_http_status(201)
+ end
+
+ it 'responds with 400 on failure and not project' do
+ expect { post v3_api("/projects/user/#{user.id}", admin) }.
+ not_to change { Project.count }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'assigns attributes to project' do
+ project = attributes_for(:project, {
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ request_access_enabled: true
+ })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker path].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_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)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_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)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_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)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_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)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::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 })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ 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
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if all discussions are resolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+ end
+
+ describe "POST /projects/:id/uploads" do
+ before { project }
+
+ it "uploads the file and returns its info" do
+ post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+
+ expect(response).to have_http_status(201)
+ expect(json_response['alt']).to eq("dk")
+ expect(json_response['url']).to start_with("/uploads/")
+ expect(json_response['url']).to end_with("/dk.png")
+ end
+ end
+
+ describe 'GET /projects/:id' do
+ context 'when unauthenticated' do
+ it 'returns the public projects' do
+ public_project = create(:empty_project, :public)
+
+ get v3_api("/projects/#{public_project.id}")
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(public_project.id)
+ expect(json_response['description']).to eq(public_project.description)
+ expect(json_response.keys).not_to include('permissions')
+ end
+ end
+
+ context 'when authenticated' do
+ before do
+ project
+ project_member
+ end
+
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ 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['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
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ 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['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ 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['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_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
+
+ it 'returns a project by path name' do
+ get v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42', user)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+ get v3_api("/projects/#{project.id}", other_user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'handles users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'exposes namespace fields' do
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['namespace']).to eq({
+ 'id' => user.namespace.id,
+ 'name' => user.namespace.name,
+ 'path' => user.namespace.path,
+ 'kind' => user.namespace.kind,
+ 'full_path' => user.namespace.full_path,
+ })
+ end
+
+ describe 'permissions' do
+ context 'all projects' do
+ before { project.team << [user, :master] }
+
+ it 'contains permission information' do
+ get v3_api("/projects", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response.first['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'personal project' do
+ it 'sets project access and returns 200' do
+ project.team << [user, :master]
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'group project' do
+ let(:project2) { create(:empty_project, group: create(:group)) }
+
+ before { project2.group.add_owner(user) }
+
+ it 'sets the owner and return 200' do
+ get v3_api("/projects/#{project2.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']).to be_nil
+ expect(json_response['permissions']['group_access']['access_level']).
+ to eq(Gitlab::Access::OWNER)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/events' do
+ shared_examples_for 'project events response' do
+ it 'returns the project events' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+ note = create(:note_on_issue, note: 'What an awesome day!', project: project)
+ EventCreateService.new.leave_note(note, note.author)
+
+ get v3_api("/projects/#{project.id}/events", current_user)
+
+ expect(response).to have_http_status(200)
+
+ first_event = json_response.first
+
+ expect(first_event['action_name']).to eq('commented on')
+ expect(first_event['note']['body']).to eq('What an awesome day!')
+
+ last_event = json_response.last
+
+ expect(last_event['action_name']).to eq('joined')
+ expect(last_event['project_id'].to_i).to eq(project.id)
+ expect(last_event['author_username']).to eq(member.username)
+ expect(last_event['author']['name']).to eq(member.name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project events response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project events response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/events", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/users' do
+ shared_examples_for 'project users response' do
+ it 'returns the project users' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+
+ get v3_api("/projects/#{project.id}/users", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+
+ first_user = json_response.first
+
+ expect(first_user['username']).to eq(member.username)
+ expect(first_user['name']).to eq(member.name)
+ expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project users response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project users response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/users', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/users", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/snippets' do
+ before { snippet }
+
+ it 'returns an array of project snippets' do
+ get v3_api("/projects/#{project.id}/snippets", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(snippet.title)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id' do
+ it 'returns a project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(snippet.title)
+ end
+
+ it 'returns a 404 error if snippet id not found' do
+ get v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/snippets' do
+ it 'creates a new project snippet' do
+ post v3_api("/projects/#{project.id}/snippets", user),
+ title: 'v3_api test', file_name: 'sample.rb', code: 'test',
+ visibility_level: '0'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('v3_api test')
+ end
+
+ it 'returns a 400 error if invalid snippet is given' do
+ post v3_api("/projects/#{project.id}/snippets", user)
+ expect(status).to eq(400)
+ end
+ end
+
+ describe 'PUT /projects/:id/snippets/:snippet_id' do
+ it 'updates an existing project snippet' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ code: 'updated code'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('example')
+ expect(snippet.reload.content).to eq('updated code')
+ end
+
+ it 'updates an existing project snippet with new title' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ title: 'other v3_api test'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('other v3_api test')
+ end
+ end
+
+ describe 'DELETE /projects/:id/snippets/:snippet_id' do
+ before { snippet }
+
+ it 'deletes existing project snippet' do
+ expect do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ end.to change { Snippet.count }.by(-1)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 when deleting unknown snippet id' do
+ delete v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id/raw' do
+ it 'gets a raw project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 error if raw project snippet not found' do
+ get v3_api("/projects/#{project.id}/snippets/5555/raw", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe :fork_admin do
+ let(:project_fork_target) { create(:empty_project) }
+ let(:project_fork_source) { create(:empty_project, :public) }
+
+ describe 'POST /projects/:id/fork/:forked_from_id' do
+ let(:new_project_fork_source) { create(:empty_project, :public) }
+
+ it "is not available for non admin users" do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'allows project to be forked from an existing project' do
+ expect(project_fork_target.forked?).not_to be_truthy
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ expect(response).to have_http_status(201)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked_project_link).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ end
+
+ it 'fails if forked_from project which does not exist' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'fails with 409 if already forked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
+ expect(response).to have_http_status(409)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked?).to be_truthy
+ end
+ end
+
+ describe 'DELETE /projects/:id/fork' do
+ it "is not visible to users outside group" do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'when users belong to project group' do
+ let(:project_fork_target) { create(:empty_project, group: create(:group)) }
+
+ before do
+ project_fork_target.group.add_owner user
+ project_fork_target.group.add_developer user2
+ end
+
+ it 'is forbidden to non-owner users' do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'makes forked project unforked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(200)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).to be_nil
+ expect(project_fork_target.forked?).not_to be_truthy
+ end
+
+ it 'is idempotent if not forked' do
+ expect(project_fork_target.forked_from_project).to be_nil
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(304)
+ expect(project_fork_target.reload.forked_from_project).to be_nil
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "shares project with group" do
+ expires_at = 10.days.from_now.to_date
+
+ expect do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['group_id']).to eq(group.id)
+ expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+ expect(json_response['expires_at']).to eq(expires_at.to_s)
+ end
+
+ it "returns a 400 error when group id is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when access level is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when user cannot read group' do
+ private_group = create(:group, :private)
+
+ post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when group does not exist' do
+ post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns a 400 error when wrong params passed" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq 'group_access does not have a valid value'
+ end
+ end
+
+ describe 'DELETE /projects/:id/share/:group_id' do
+ it 'returns 204 when deleting a group share' do
+ group = create(:group, :public)
+ create(:project_group_link, group: group, project: project)
+
+ delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_http_status(204)
+ expect(project.project_group_links).to be_empty
+ end
+
+ it 'returns a 400 when group id is not an integer' do
+ delete v3_api("/projects/#{project.id}/share/foo", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when group link does not exist' do
+ delete v3_api("/projects/#{project.id}/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when project does not exist' do
+ delete v3_api("/projects/123/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/search/:query' do
+ let!(:query) { 'query'}
+ let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
+ let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
+ let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
+ let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
+ let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
+ let!(:public) { create(:empty_project, :public, name: "public #{query}") }
+ let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
+
+ shared_examples_for 'project search response' do |args = {}|
+ it 'returns project search responses' do
+ get v3_api("/projects/search/#{args[:query]}", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(args[:results])
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 1 do
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 6 do
+ let(:current_user) { user }
+ end
+ it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
+ let(:current_user) { user2 }
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id' do
+ before { project }
+ before { user }
+ before { user3 }
+ before { user4 }
+ before { project3 }
+ before { project4 }
+ before { project_member3 }
+ before { project_member2 }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.id}"), project_param
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'updates name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.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
+ end
+
+ it 'updates visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_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
+ end
+
+ it 'updates visibility_level from public to private' do
+ project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project_param = { public: false }
+ put v3_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)
+ end
+
+ it 'does not update name to existing name' do
+ project_param = { name: project3.name }
+ put v3_api("/projects/#{project.id}", user), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['name']).to eq(['has already been taken'])
+ end
+
+ it 'updates request_access_enabled' do
+ project_param = { request_access_enabled: false }
+
+ put v3_api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+ expect(json_response['request_access_enabled']).to eq(false)
+ end
+
+ it 'updates path & name to existing path & name in different namespace' do
+ project_param = { path: project4.path, name: project4.name }
+ put v3_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
+ end
+ end
+
+ context 'when authenticated as project master' do
+ it 'updates path' do
+ project_param = { path: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), 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
+ end
+
+ it 'updates other attributes' do
+ project_param = { issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description' }
+
+ put v3_api("/projects/#{project3.id}", user4), 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
+ end
+
+ it 'does not update path to existing path' do
+ project_param = { path: project.path }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['path']).to eq(['has already been taken'])
+ end
+
+ it 'does not update name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not update visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as project developer' do
+ it 'does not update other attributes' do
+ project_param = { path: 'bar',
+ issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description',
+ request_access_enabled: true }
+ put v3_api("/projects/#{project.id}", user3), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/archive' do
+ context 'on an unarchived project' do
+ it 'archives the project' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'remains archived' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/archive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/unarchive' do
+ context 'on an unarchived project' do
+ it 'remains unarchived' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'unarchives the project' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/unarchive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/star' do
+ context 'on an unstarred project' do
+ it 'stars the project' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['star_count']).to eq(1)
+ end
+ end
+
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'does not modify the star count' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/star' do
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'unstars the project' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['star_count']).to eq(0)
+ end
+ end
+
+ context 'on an unstarred project' do
+ it 'does not modify the star count' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id' do
+ context 'when authenticated as user' do
+ it 'removes project' do
+ delete v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a project if not an owner' do
+ user3 = create(:user)
+ project.team << [user3, :developer]
+ delete v3_api("/projects/#{project.id}", user3)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not remove a project not attached to user' do
+ delete v3_api("/projects/#{project.id}", user2)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'removes any existing project' do
+ delete v3_api("/projects/#{project.id}", admin)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', admin)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
new file mode 100644
index 00000000000..fef6fb641fa
--- /dev/null
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -0,0 +1,366 @@
+require 'spec_helper'
+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) } }
+ let!(:project) { create(:project, :repository, creator: user) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+ describe "GET /projects/:id/repository/tree" do
+ let(:route) { "/projects/#{project.id}/repository/tree" }
+
+ shared_examples_for 'repository tree' do
+ it 'returns the repository tree' do
+ get v3_api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ first_commit = json_response.first
+ expect(first_commit['name']).to eq('bar')
+ expect(first_commit['type']).to eq('tree')
+ expect(first_commit['mode']).to eq('040000')
+ end
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?ref_name=foo", current_user) }
+ let(:message) { '404 Tree 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
+
+ context 'with recursive=1' do
+ it 'returns recursive project paths tree' do
+ get v3_api("#{route}?recursive=1", current_user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ 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
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) }
+ let(:message) { '404 Tree Not Found' }
+ end
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository tree' 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 tree' 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
+
+ {
+ '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" }
+
+ shared_examples_for 'repository contributors' do
+ it 'returns valid data' do
+ get v3_api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ first_contributor = json_response.first
+ expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
+ expect(first_contributor['name']).to eq('tiagonbotelho')
+ expect(first_contributor['commits']).to eq(1)
+ expect(first_contributor['additions']).to eq(0)
+ expect(first_contributor['deletions']).to eq(0)
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository contributors' 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 contributors' 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
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
new file mode 100644
index 00000000000..91038977c82
--- /dev/null
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe API::V3::SystemHooks, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:hook) { create(:system_hook, url: "http://example.com") }
+
+ before { stub_request(:post, hook.url) }
+
+ describe "GET /hooks" do
+ context "when no user" do
+ it "returns authentication error" do
+ get v3_api("/hooks")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when not an admin" do
+ it "returns forbidden error" do
+ get v3_api("/hooks", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns an array of hooks" do
+ get v3_api("/hooks", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['url']).to eq(hook.url)
+ expect(json_response.first['push_events']).to be true
+ expect(json_response.first['tag_push_events']).to be false
+ 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
new file mode 100644
index 00000000000..6870cfd2668
--- /dev/null
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Tags, api: true do
+ include ApiHelpers
+ include RepoHelpers
+
+ 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) }
+
+ describe "GET /projects/:id/repository/tags" do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:description) { 'Awesome release!' }
+
+ shared_examples_for 'repository tags' do
+ it 'returns the repository tags' do
+ get v3_api("/projects/#{project.id}/repository/tags", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tags' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'repository tags' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'without releases' do
+ it "returns an array of project tags" do
+ get v3_api("/projects/#{project.id}/repository/tags", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'with releases' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it "returns an array of project tags with release info" do
+ get v3_api("/projects/#{project.id}/repository/tags", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['message']).to eq('Version 1.1.0')
+ expect(json_response.first['release']['description']).to eq(description)
+ 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
new file mode 100644
index 00000000000..f1e554b98cc
--- /dev/null
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -0,0 +1,203 @@
+require 'spec_helper'
+
+describe API::V3::Templates, api: true do
+ include ApiHelpers
+
+ shared_examples_for 'the Template Entity' do |path|
+ before { get v3_api(path) }
+
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ shared_examples_for 'the TemplateList Entity' do |path|
+ before { get v3_api(path) }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ shared_examples_for 'requesting gitignores' do |path|
+ it 'returns a list of available gitignore templates' do
+ get v3_api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
+ end
+
+ shared_examples_for 'requesting gitlab-ci-ymls' do |path|
+ it 'returns a list of available gitlab_ci_ymls' do
+ get v3_api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
+ end
+
+ shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path|
+ it 'adds a disclaimer on the top' do
+ get v3_api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+
+ shared_examples_for 'the License Template Entity' do |path|
+ before { get v3_api(path) }
+
+ it 'returns a license template' do
+ expect(json_response['key']).to eq('mit')
+ expect(json_response['name']).to eq('MIT License')
+ expect(json_response['nickname']).to be_nil
+ 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 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('MIT License')
+ end
+ end
+
+ shared_examples_for 'GET licenses' do |path|
+ it 'returns a list of available license templates' do
+ get v3_api(path)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(12)
+ expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+ end
+
+ describe 'the popular parameter' do
+ context 'with popular=1' do
+ it 'returns a list of available popular license templates' do
+ get v3_api("#{path}?popular=1")
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
+ end
+ end
+ end
+ end
+
+ shared_examples_for 'GET licenses/:name' do |path|
+ context 'with :project and :fullname given' do
+ before do
+ get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ end
+
+ context 'for the mit license' do
+ let(:license_type) { 'mit' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('MIT License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the agpl-3.0 license' do
+ let(:license_type) { 'agpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-3.0 license' do
+ let(:license_type) { 'gpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the gpl-2.0 license' do
+ let(:license_type) { 'gpl-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for the apache-2.0 license' do
+ let(:license_type) { 'apache-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('Apache License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
+ end
+ end
+
+ context 'for an uknown license' do
+ let(:license_type) { 'muth-over9000' }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with no :fullname given' do
+ context 'with an authenticated user' do
+ let(:user) { create(:user) }
+
+ it 'replaces the copyright owner placeholder with the name of the current user' do
+ get v3_api('/templates/licenses/mit', user)
+
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
+ end
+ end
+ end
+ end
+
+ describe 'with /templates namespace' do
+ it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/templates/gitignores'
+ it_behaves_like 'requesting gitignores', '/templates/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/templates/licenses/mit'
+ it_behaves_like 'GET licenses', '/templates/licenses'
+ it_behaves_like 'GET licenses/:name', '/templates/licenses'
+ end
+
+ describe 'without /templates namespace' do
+ it_behaves_like 'the Template Entity', '/gitignores/Ruby'
+ it_behaves_like 'the TemplateList Entity', '/gitignores'
+ it_behaves_like 'requesting gitignores', '/gitignores'
+ it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls'
+ it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby'
+ it_behaves_like 'the License Template Entity', '/licenses/mit'
+ it_behaves_like 'GET licenses', '/licenses'
+ it_behaves_like 'GET licenses/:name', '/licenses'
+ end
+end
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
new file mode 100644
index 00000000000..80fa697e949
--- /dev/null
+++ b/spec/requests/api/v3/todos_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe API::V3::Todos, api: true do
+ include ApiHelpers
+
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
+ let(:author_1) { create(:user) }
+ let(:author_2) { create(:user) }
+ let(:john_doe) { create(:user, username: 'john_doe') }
+ 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!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
+
+ before do
+ project_1.team << [john_doe, :developer]
+ project_2.team << [john_doe, :developer]
+ end
+
+ describe 'DELETE /todos/:id' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete v3_api("/todos/#{pending_1.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks a todo as done' do
+ delete v3_api("/todos/#{pending_1.id}", john_doe)
+
+ expect(response.status).to eq(200)
+ expect(pending_1.reload).to be_done
+ end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete v3_api("/todos/#{pending_1.id}", john_doe)
+ end
+ end
+ end
+
+ describe 'DELETE /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete v3_api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks all todos as done' do
+ delete v3_api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('3')
+ expect(pending_1.reload).to be_done
+ expect(pending_2.reload).to be_done
+ expect(pending_3.reload).to be_done
+ end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete v3_api("/todos", john_doe)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
new file mode 100644
index 00000000000..4819269d69f
--- /dev/null
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -0,0 +1,218 @@
+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 'creates builds on webhook from other gitlab repository and branch' 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
+
+ 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
+ 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
new file mode 100644
index 00000000000..17bbb0b53c1
--- /dev/null
+++ b/spec/requests/api/v3/users_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe API::V3::Users, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:key) { create(:key, user: user) }
+ let(:email) { create(:email, user: user) }
+ let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+
+ describe 'GET /user/:id/keys' do
+ before { admin }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api("/users/#{user.id}/keys")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get v3_api('/users/999999/keys', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns array of ssh keys' do
+ user.keys << key
+ user.save
+
+ get v3_api("/users/#{user.id}/keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(key.title)
+ end
+ end
+ end
+
+ describe 'GET /user/:id/emails' do
+ before { admin }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api("/users/#{user.id}/emails")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get v3_api('/users/999999/emails', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns array of emails' do
+ user.emails << email
+ user.save
+
+ get v3_api("/users/#{user.id}/emails", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['email']).to eq(email.email)
+ end
+
+ it "returns a 404 for invalid ID" do
+ put v3_api("/users/ASDF/emails", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "GET /user/keys" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/user/keys")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns array of ssh keys" do
+ user.keys << key
+ user.save
+
+ get v3_api("/user/keys", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first["title"]).to eq(key.title)
+ end
+ end
+ end
+
+ describe "GET /user/emails" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/user/emails")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns array of emails" do
+ user.emails << email
+ user.save
+
+ get v3_api("/user/emails", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first["email"]).to eq(email.email)
+ end
+ end
+ end
+
+ describe 'PUT /users/:id/block' do
+ before { admin }
+ it 'blocks existing user' do
+ put v3_api("/users/#{user.id}/block", admin)
+ expect(response).to have_http_status(200)
+ expect(user.reload.state).to eq('blocked')
+ end
+
+ it 'does not re-block ldap blocked users' do
+ put v3_api("/users/#{ldap_blocked_user.id}/block", admin)
+ expect(response).to have_http_status(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
+ it 'does not be available for non admin users' do
+ put v3_api("/users/#{user.id}/block", user)
+ expect(response).to have_http_status(403)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'returns a 404 error if user id not found' do
+ put v3_api('/users/9999/block', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'PUT /users/:id/unblock' do
+ let(:blocked_user) { create(:user, state: 'blocked') }
+ before { admin }
+
+ it 'unblocks existing user' do
+ put v3_api("/users/#{user.id}/unblock", admin)
+ expect(response).to have_http_status(200)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'unblocks a blocked user' do
+ put v3_api("/users/#{blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(200)
+ expect(blocked_user.reload.state).to eq('active')
+ end
+
+ it 'does not unblock ldap blocked users' do
+ put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
+ it 'does not be available for non admin users' do
+ put v3_api("/users/#{user.id}/unblock", user)
+ expect(response).to have_http_status(403)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'returns a 404 error if user id not found' do
+ put v3_api('/users/9999/block', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it "returns a 404 for invalid ID" do
+ put v3_api("/users/ASDF/block", admin)
+
+ 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 1cedaa4ba63..9948d1a9ea0 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 }
@@ -288,7 +288,7 @@ describe Ci::API::Builds do
expect(build.reload.trace).to eq 'BUILD TRACE'
end
- context 'build has been erased' do
+ context 'job has been erased' do
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
it 'responds with forbidden' do
@@ -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/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index a30be767119..5321f8b134f 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -60,7 +60,8 @@ describe Ci::API::Triggers do
it 'validates variables to be a hash' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('variables needs to be a hash')
+
+ expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 4a16824de04..006d6a6af1c 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -57,7 +57,7 @@ describe 'Git HTTP requests', lib: true do
end
context 'but the repo is disabled' do
- let(:project) { create(:project, repository_access_level: ProjectFeature::DISABLED, wiki_access_level: ProjectFeature::ENABLED) }
+ let(:project) { create(:project, :repository_disabled, :wiki_enabled) }
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.path_with_namespace}.git" }
@@ -141,7 +141,7 @@ describe 'Git HTTP requests', lib: true do
context 'when the repo is public' do
context 'but the repo is disabled' do
it 'does not allow to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)
+ project = create(:project, :public, :repository_disabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -151,7 +151,7 @@ describe 'Git HTTP requests', lib: true do
context 'but the repo is enabled' do
it 'allows to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
+ project = create(:project, :public, :repository_enabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:ok)
@@ -161,7 +161,7 @@ describe 'Git HTTP requests', lib: true do
context 'but only project members are allowed' do
it 'does not allow to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)
+ project = create(:project, :public, :repository_private)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -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
@@ -360,10 +368,6 @@ describe 'Git HTTP requests', lib: true do
let(:project) { build.project }
let(:other_project) { create(:empty_project) }
- before do
- project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
- end
-
context 'when build created by system is authenticated' do
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 9bfc84c7425..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,16 +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,
- '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
@@ -625,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
@@ -664,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
@@ -687,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
@@ -788,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 77549db2927..4baccacd448 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe 'project routing' do
before do
- allow(Project).to receive(:find_with_namespace).and_return(false)
- allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true)
+ allow(Project).to receive(:find_by_full_path).and_return(false)
+ allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true)
end
# Shared examples for a resource inside a Project
@@ -27,35 +27,42 @@ describe 'project routing' do
# let(:actions) { [:index] }
# let(:controller) { 'issues' }
# end
+ #
+ # # Different controller name and path
+ # it_behaves_like 'RESTful project resources' do
+ # let(:controller) { 'pages_domains' }
+ # let(:controller_path) { 'pages/domains' }
+ # end
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
+ let(:controller_path) { controller }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
end
end
@@ -86,13 +93,13 @@ describe 'project routing' do
end
context 'name with dot' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end
context 'with nested group' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end
@@ -113,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
@@ -121,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
@@ -424,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
@@ -539,4 +555,20 @@ describe 'project routing' do
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ describe Projects::PagesDomainsController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:show, :new, :create, :destroy] }
+ let(:controller) { 'pages_domains' }
+ let(:controller_path) { 'pages/domains' }
+ end
+
+ it 'to #destroy with a valid domain name' do
+ expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+
+ it 'to #show with a valid domain' do
+ expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+ 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/gem_fetcher_spec.rb b/spec/rubocop/cop/gem_fetcher_spec.rb
new file mode 100644
index 00000000000..c07f6a831dc
--- /dev/null
+++ b/spec/rubocop/cop/gem_fetcher_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/gem_fetcher'
+
+describe RuboCop::Cop::GemFetcher do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in Gemfile' do
+ before do
+ allow(cop).to receive(:gemfile?).and_return(true)
+ end
+
+ it 'registers an offense when a gem uses `git`' do
+ inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['git: "https://gitlab.com/foo/bar.git"'])
+ end
+ end
+
+ it 'registers an offense when a gem uses `github`' do
+ inspect_source(cop, 'gem "foo", github: "foo/bar.git"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['github: "foo/bar.git"'])
+ end
+ end
+ end
+
+ context 'outside of Gemfile' do
+ it 'registers no offense' do
+ inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
new file mode 100644
index 00000000000..6b9b6b19650
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_column_with_default'
+
+describe RuboCop::Cop::Migration::AddColumnWithDefault 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_column_with_default is used inside a change method' do
+ inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; 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_column_with_default is used inside an up method' do
+ inspect_source(cop, 'def up; add_column_with_default :table, :column, default: false; 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_column_with_default :table, :column, default: false; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
new file mode 100644
index 00000000000..7cb24dc5646
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key'
+
+describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
+ include CopHelper
+
+ let(:cop) { described_class.new }
+
+ context 'outside of a migration' do
+ it 'does not register any offenses' do
+ inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'in a migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when using add_foreign_key' do
+ inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ 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/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 3c37660885d..6a6df377b35 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -52,4 +52,147 @@ describe EnvironmentSerializer do
expect(json).to be_an_instance_of Array
end
end
+
+ context 'when representing environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project).within_folders
+ end
+
+ let(:resource) { Environment.all }
+
+ subject { serializer.represent(resource) }
+
+ context 'when there is a single environment' do
+ before { create(:environment, name: 'staging') }
+
+ it 'represents one standalone environment' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple environments in folder' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ end
+
+ it 'represents one item that is a folder' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 2
+ expect(subject.first[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.first[:latest][:environment_type]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple folders and standalone environments' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ create(:environment, name: 'production/my-review-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'represents multiple items grouped within folders' do
+ expect(subject.count).to eq 3
+
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'production/my-review-3'
+ expect(subject.first[:latest][:environment_type]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ expect(subject.second[:size]).to eq 2
+ expect(subject.second[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.second[:latest][:environment_type]).to eq 'staging'
+ expect(subject.third[:name]).to eq 'testing'
+ expect(subject.third[:size]).to eq 1
+ expect(subject.third[:latest][:name]).to eq 'testing'
+ expect(subject.third[:latest][:environment_type]).to be_nil
+ end
+ end
+ end
+
+ context 'when used with pagination' do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:resource) { Environment.all }
+ let(:pagination) { { page: 1, per_page: 2 } }
+
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ end
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(pagination)
+ end
+
+ subject { serializer.represent(resource) }
+
+ it 'creates a paginated serializer' do
+ expect(serializer).to be_paginated
+ end
+
+ context 'when resource is paginatable relation' do
+ context 'when there is a single environment object in relation' do
+ before { create(:environment) }
+
+ it 'serializes environments' do
+ expect(subject.first).to have_key :id
+ end
+ end
+
+ context 'when multiple environment objects are serialized' do
+ before { create_list(:environment, 3) }
+
+ it 'serializes appropriate number of objects' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(response).to receive(:[]=).with('X-Total', '3')
+ expect(response).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(response).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when grouping environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ .within_folders
+ end
+
+ before do
+ create(:environment, name: 'staging/review-1')
+ create(:environment, name: 'staging/review-2')
+ create(:environment, name: 'production/deploy-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'paginates grouped items including ordering' do
+ expect(subject.count).to eq 2
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ end
+
+ it 'appends correct total page count header' do
+ expect(subject).not_to be_empty
+ expect(response).to have_received(:[]=).with('X-Total', '3')
+ end
+
+ it 'appends correct page count headers' do
+ expect(subject).not_to be_empty
+ expect(response).to have_received(:[]=).with('X-Total-Pages', '2')
+ expect(response).to have_received(:[]=).with('X-Per-Page', '2')
+ end
+ end
+ end
+ end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 7cbf131e41e..2aaef03cb93 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -52,14 +52,14 @@ describe PipelineSerializer do
expect(serializer).to be_paginated
end
- context 'when resource does is not paginatable' do
+ context 'when resource is not paginatable' do
context 'when a single pipeline object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) }
let(:pagination) { { page: 1, per_page: 1 } }
it 'raises error' do
- expect { subject }
- .to raise_error(PipelineSerializer::InvalidResourceError)
+ expect { subject }.to raise_error(
+ Gitlab::Serializer::Pagination::InvalidResourceError)
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/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index fde807cc410..7b29b043296 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do
expect { service.execute }.to change(Board, :count).by(1)
end
- it 'creates default lists' do
+ it 'creates the default lists' do
board = service.execute
- expect(board.lists.size).to eq 2
- expect(board.lists.first).to be_backlog
- expect(board.lists.last).to be_done
+ expect(board.lists.size).to eq 1
+ expect(board.lists.first).to be_done
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 7c206cf3ce7..01baedc4761 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
@@ -44,32 +43,6 @@ 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 listing issues from Backlog' do
- params = { board_id: board.id, id: backlog.id }
-
- issues = described_class.new(project, user, params).execute
-
- expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
- end
-
- it 'returns closed issues when listing issues from Done' do
- params = { board_id: board.id, id: done.id }
-
- issues = described_class.new(project, user, params).execute
-
- expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
- end
-
- it 'returns opened issues that have label list applied when listing issues from a label list' do
- params = { board_id: board.id, id: list1.id }
-
- issues = described_class.new(project, user, params).execute
-
- expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
- end
- end
-
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index c43b2aec490..727ea04ea5c 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { create(:backlog_list, board: board1) }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) }
@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do
project.team << [user, :developer]
end
- context 'when moving from backlog' do
- it 'adds the label of the list it goes to' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug, development)
- end
- end
-
- context 'when moving to backlog' do
- it 'removes all list-labels' do
- issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
- params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug)
- end
- end
-
- context 'when moving from backlog to done' do
- it 'closes the issue' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_closed
- end
- end
-
context 'when moving an issue between lists' 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: list2.id } }
@@ -113,22 +77,11 @@ describe Boards::Issues::MoveService, services: true do
end
end
- context 'when moving from done to backlog' do
- it 'reopens the issue' do
- issue = create(:labeled_issue, :closed, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_reopened
- end
- 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
@@ -139,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/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index a7e9efcf93f..ebac38e68f1 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, and done lists' do
+ context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, label and done lists' do
+ context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 628caf03476..a30860f828a 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do
described_class.new(project, user).execute(development)
- expect(backlog.reload.position).to be_nil
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(done.reload.position).to be_nil
end
end
- it 'does not remove list from board when list type is backlog' do
- list = board.backlog_list
- service = described_class.new(project, user)
-
- expect { service.execute(list) }.not_to change(board.lists, :count)
- end
-
it 'does not remove list from board when list type is done' do
list = board.done_list
service = described_class.new(project, user)
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 334cee3f06d..2dffc62b215 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double)
- expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+ expect(service.execute(board)).to eq [list, board.done_list]
end
end
end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 63fa0bb8c5f..3786dc82bf0 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do
end
end
- it 'keeps position of lists when list type is backlog' do
- service = described_class.new(project, user, position: 2)
-
- service.execute(backlog)
-
- expect(current_list_positions).to eq [0, 1, 2, 3]
- end
-
it 'keeps position of lists when list type is done' do
service = described_class.new(project, user, position: 2)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ceaca96e25b..8459a3d8cfb 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,
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 ebb11166964..d93616c4f50 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,384 +1,529 @@
require 'spec_helper'
-describe Ci::ProcessPipelineService, services: true do
- let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') }
+describe Ci::ProcessPipelineService, '#execute', :services do
let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
- 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
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: 'master', project: project)
+ end
- it 'processes a pipeline' do
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(2)
+ before do
+ project.add_developer(user)
+ end
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(4)
+ 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)
+ 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)
+
+ 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(process_pipeline).to be_falsey
+ 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)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure)
+ expect(builds_statuses).to eq %w(success failed pending)
- expect(process_pipeline).to be_falsey
- expect(builds.pending.count).to eq(2)
+ 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
+
+ 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)
- pipeline.builds.running_or_pending.each(&:drop)
- expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+ 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 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 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
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ context 'when first stage succeeds' do
+ it 'blocks pipeline on stage with first manual action' do
+ process_pipeline
- builds.first.success
- builds.each(&:reload)
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ 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
end
+ end
+
+ context 'when first stage fails' do
+ it 'does not take blocking action into account' do
+ process_pipeline
- 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
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ fail_running_or_pending
- builds.first.success
- builds.each(&:reload)
+ expect(builds_names).to eq %w[code:test production:test]
+ expect(builds_statuses).to eq %w[failed pending]
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ succeed_running_or_pending
+
+ 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]
- 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')
+ succeed_running_or_pending
- pipeline.builds.running_or_pending.each(&:success)
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy]
+ expect(builds_statuses).to eq %w[success success success manual]
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ 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
- pipeline.builds.find_by(name: 'test:1').success
- pipeline.builds.find_by(name: 'test:2').drop
+ play_manual_action('production:deploy')
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ expect(builds_statuses).to eq %w[success success success pending]
+ expect(pipeline.reload).to be_running
- Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+ succeed_running_or_pending
- expect(builds.pluck(:name)).to contain_exactly(
- 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
- end
+ 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
+
+ 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
new file mode 100644
index 00000000000..65af4e13118
--- /dev/null
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Ci::RetryBuildService, :services do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:service) 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 gl_project_id deploy job_id 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_expired, :erased,
+ :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
+ :teardown_environment, :triggered, :trace,
+ description: 'some build', pipeline: pipeline)
+ end
+
+ 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 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
+ end
+ end
+
+ it 'has correct number of known attributes' do
+ 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(known_accessors).to contain_exactly(*current_accessors)
+ end
+ end
+
+ describe '#execute' do
+ let(:new_build) { service.execute(build) }
+
+ context 'when user has ability to execute build' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'build duplication'
+
+ it 'creates a new build that represents the old one' do
+ expect(new_build.name).to eq build.name
+ end
+
+ it 'enqueues the new build' do
+ expect(new_build).to be_pending
+ end
+
+ it 'resolves todos for old build that failed' do
+ expect(MergeRequests::AddTodoWhenBuildFailsService)
+ .to receive_message_chain(:new, :close)
+
+ service.execute(build)
+ end
+
+ context 'when there are subsequent builds that are skipped' do
+ let!(:subsequent_build) do
+ create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline)
+ end
+
+ it 'resumes pipeline processing in subsequent stages' do
+ service.execute(build)
+
+ expect(subsequent_build.reload).to be_created
+ end
+ end
+ end
+
+ context 'when user does not have ability to execute build' do
+ it 'raises an error' do
+ expect { service.execute(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+ end
+
+ describe '#reprocess' do
+ let(:new_build) { service.reprocess(build) }
+
+ context 'when user has ability to execute build' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'build duplication'
+
+ it 'creates a new build that represents the old one' do
+ expect(new_build.name).to eq build.name
+ end
+
+ it 'does not enqueue the new build' do
+ expect(new_build).to be_created
+ end
+ end
+
+ context 'when user does not have ability to execute build' do
+ it 'raises an error' do
+ expect { service.reprocess(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
new file mode 100644
index 00000000000..5445b65f4e8
--- /dev/null
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+describe Ci::RetryPipelineService, '#execute', :services do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:service) { described_class.new(project, user) }
+
+ context 'when user has ability to modify pipeline' do
+ let(:user) { create(:admin) }
+
+ context 'when there are failed builds in the last stage' do
+ before do
+ create_build('rspec 1', :success, 0)
+ create_build('rspec 2', :failed, 1)
+ create_build('rspec 3', :canceled, 1)
+ end
+
+ it 'enqueues all builds in the last stage' do
+ service.execute(pipeline)
+
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_pending
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there are failed or canceled builds in the first stage' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('rspec 2', :canceled, 0)
+ create_build('rspec 3', :canceled, 1)
+ create_build('spinach 1', :canceled, 2)
+ end
+
+ it 'retries builds failed builds and marks subsequent for processing' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_created
+ expect(build('spinach 1')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is failed build present which was run on failure' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('rspec 2', :canceled, 0)
+ create_build('rspec 3', :canceled, 1)
+ create_build('report 1', :failed, 2)
+ end
+
+ it 'retries builds only in the first stage' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_created
+ expect(build('report 1')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+
+ it 'creates a new job for report job in this case' do
+ service.execute(pipeline)
+
+ expect(statuses.where(name: 'report 1').first).to be_retried
+ 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 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
+
+ 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
+
+ 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('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 reprocesses manual actions' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_manual
+ expect(build('staging')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ 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, when: :manual, allow_failure: true)
+ end
+
+ it 'retries canceled job and does not update the manual action' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ 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, when: :manual, allow_failure: true)
+ end
+
+ 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_manual
+ expect(pipeline.reload).to be_running
+ end
+ end
+ end
+
+ it 'closes all todos about failed jobs for pipeline' do
+ expect(MergeRequests::AddTodoWhenBuildFailsService)
+ .to receive_message_chain(:new, :close_all)
+
+ service.execute(pipeline)
+ end
+
+ it 'reprocesses the pipeline' do
+ expect(pipeline).to receive(:process!)
+
+ service.execute(pipeline)
+ end
+ end
+
+ context 'when user is not allowed to retry pipeline' do
+ it 'raises an error' do
+ expect { service.execute(pipeline) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ def statuses
+ pipeline.reload.statuses
+ end
+
+ def build(name)
+ statuses.latest.find_by(name: name)
+ end
+
+ def create_build(name, status, stage_num, **opts)
+ create(:ci_build, name: name,
+ status: status,
+ stage: "stage_#{stage_num}",
+ stage_idx: stage_num,
+ pipeline: pipeline, **opts) do |build|
+ pipeline.update_status
+ end
+ end
+end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 6f7d1a5d28d..560f83d94f7 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -42,10 +42,10 @@ describe Ci::StopEnvironmentsService, services: true do
end
end
- context 'when environment is not stoppable' do
+ context 'when environment is not stopped' do
before do
allow_any_instance_of(Environment)
- .to receive(:stoppable?).and_return(false)
+ .to receive(:state).and_return(:stopped)
end
it 'does not stop environment' do
diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb
new file mode 100644
index 00000000000..e429fcfc72f
--- /dev/null
+++ b/spec/services/ci/update_runner_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Ci::UpdateRunnerService, :services do
+ let(:runner) { create(:ci_runner) }
+
+ describe '#update' do
+ before do
+ allow(runner).to receive(:tick_runner_queue)
+ end
+
+ context 'with description params' do
+ let(:params) { { description: 'new runner' } }
+
+ it 'updates the runner and ticking the queue' do
+ expect(update).to be_truthy
+
+ runner.reload
+
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.description).to eq('new runner')
+ end
+ end
+
+ context 'when params are not valid' do
+ let(:params) { { run_untagged: false } }
+
+ it 'does not update and give false because it is not valid' do
+ expect(update).to be_falsey
+
+ runner.reload
+
+ expect(runner).not_to have_received(:tick_runner_queue)
+ expect(runner.run_untagged).to be_truthy
+ end
+ end
+
+ def update
+ described_class.new(runner).update(params)
+ end
+ end
+end
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 3760f19aaa2..0a7fc58523f 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe CompareService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:service) { described_class.new }
+ let(:service) { described_class.new(project, 'feature') }
describe '#execute' do
context 'compare with base, like feature...fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+ subject { service.execute(project, 'fix', straight: false) }
it { expect(subject.diffs.size).to eq(1) }
end
context 'straight compare, like feature..fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+ subject { service.execute(project, 'fix', straight: true) }
it { expect(subject.diffs.size).to eq(3) }
end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index cf0a18aacec..18b964e2453 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,7 +103,8 @@ 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_BUILD_REF_NAME',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -234,7 +237,11 @@ describe CreateDeploymentService, services: true do
context 'when build is retried' do
it_behaves_like 'does create environment and deployment' do
- let(:deployable) { Ci::Build.retry(build) }
+ before do
+ project.add_developer(user)
+ end
+
+ let(:deployable) { Ci::Build.retry(build, user) }
subject { deployable.success }
end
diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/create_tag_service_spec.rb
deleted file mode 100644
index 7dc43c50b0d..00000000000
--- a/spec/services/create_tag_service_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'spec_helper'
-
-describe CreateTagService, services: true do
- let(:project) { create(:project) }
- let(:repository) { project.repository }
- let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'creates the tag and returns success' do
- response = service.execute('v42.42.42', 'master', 'Foo')
-
- expect(response[:status]).to eq(:success)
- expect(response[:tag]).to be_a Gitlab::Git::Tag
- expect(response[:tag].name).to eq('v42.42.42')
- end
-
- context 'when target is invalid' do
- it 'returns an error' do
- response = service.execute('v1.1.0', 'foo', 'Foo')
-
- expect(response).to eq(status: :error,
- message: 'Target foo is invalid')
- end
- end
-
- context 'when tag already exists' do
- it 'returns an error' do
- expect(repository).to receive(:add_tag).
- with(user, 'v1.1.0', 'master', 'Foo').
- and_raise(Rugged::TagError)
-
- response = service.execute('v1.1.0', 'master', 'Foo')
-
- expect(response).to eq(status: :error,
- message: 'Tag v1.1.0 already exists')
- end
- end
-
- context 'when pre-receive hook fails' do
- it 'returns an error' do
- expect(repository).to receive(:add_tag).
- with(user, 'v1.1.0', 'master', 'Foo').
- and_raise(GitHooksService::PreReceiveError, 'something went wrong')
-
- response = service.execute('v1.1.0', 'master', 'Foo')
-
- expect(response).to eq(status: :error,
- message: 'something went wrong')
- end
- end
- end
-end
diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb
deleted file mode 100644
index 477551f5036..00000000000
--- a/spec/services/delete_tag_service_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'spec_helper'
-
-describe DeleteTagService, services: true do
- let(:project) { create(:project) }
- let(:repository) { project.repository }
- let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'removes the tag' do
- expect(repository).to receive(:before_remove_tag)
- expect(service).to receive(:success)
-
- service.execute('v1.1.0')
- end
- end
-end
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
deleted file mode 100644
index 418a12a83a9..00000000000
--- a/spec/services/delete_user_service_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'spec_helper'
-
-describe DeleteUserService, services: true do
- describe "Deletes a user and all their personal projects" do
- let!(:user) { create(:user) }
- let!(:current_user) { create(:user) }
- let!(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace) }
-
- context 'no options are given' do
- it 'deletes the user' do
- user_data = DeleteUserService.new(current_user).execute(user)
-
- expect { user_data['email'].to eq(user.email) }
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'will delete the project in the near future' do
- expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
-
- DeleteUserService.new(current_user).execute(user)
- end
- end
-
- context "solo owned groups present" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- DeleteUserService.new(current_user).execute(user)
- end
-
- it 'does not delete the user' do
- expect(User.find(user.id)).to eq user
- end
- end
-
- context "deletions with solo owned groups" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true)
- end
-
- it 'deletes solo owned groups' do
- expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'deletes the user' do
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
- end
-end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
deleted file mode 100644
index 538e85cdc89..00000000000
--- a/spec/services/destroy_group_service_spec.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-require 'spec_helper'
-
-describe DestroyGroupService, services: true do
- include DatabaseConnectionHelpers
-
- let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:project) { create(:project, namespace: group) }
- let!(:gitlab_shell) { Gitlab::Shell.new }
- let!(:remove_path) { group.path + "+#{group.id}+deleted" }
-
- shared_examples 'group destruction' do |async|
- context 'database records' do
- before do
- destroy_group(group, user, async)
- end
-
- it { expect(Group.all).not_to include(group) }
- it { expect(Project.all).not_to include(project) }
- end
-
- context 'file system' do
- context 'Sidekiq inline' do
- before do
- # Run sidekiq immediatly to check that renamed dir will be removed
- Sidekiq::Testing.inline! { destroy_group(group, user, async) }
- end
-
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
- end
-
- context 'Sidekiq fake' do
- before do
- # Dont run sidekiq to check if renamed repository exists
- Sidekiq::Testing.fake! { destroy_group(group, user, async) }
- end
-
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
- end
- end
-
- def destroy_group(group, user, async)
- if async
- DestroyGroupService.new(group, user).async_execute
- else
- DestroyGroupService.new(group, user).execute
- end
- end
- end
-
- describe 'asynchronous delete' do
- it_behaves_like 'group destruction', true
-
- context 'potential race conditions' do
- context "when the `GroupDestroyWorker` task runs immediately" do
- it "deletes the group" do
- # Commit the contents of this spec's transaction so far
- # so subsequent db connections can see it.
- #
- # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
- # transaction is currently in progress". Without this, this
- # spec will always be green, since the group created in setup
- # cannot be seen by any other connections / threads in this spec.
- Group.connection.commit_db_transaction
-
- group_record = run_with_new_database_connection do |conn|
- conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
- end
-
- expect(group_record).not_to be_nil
-
- # Execute the contents of `GroupDestroyWorker` in a separate thread, to
- # simulate data manipulation by the Sidekiq worker (different database
- # connection / transaction).
- expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
- Thread.new { m[group_id, user_id] }.join(5)
- end
-
- # Kick off the initial group destroy in a new thread, so that
- # it doesn't share this spec's database transaction.
- Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5)
-
- group_record = run_with_new_database_connection do |conn|
- conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
- end
-
- expect(group_record).to be_nil
- end
- end
- end
- end
-
- describe 'synchronous delete' do
- it_behaves_like 'group destruction', false
- end
-end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d3c37c7820f..35e6e139238 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -6,7 +6,10 @@ describe Files::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:file_path) { 'files/ruby/popen.rb' }
- let(:new_contents) { "New Content" }
+ let(:new_contents) { 'New Content' }
+ let(:target_branch) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
let(:commit_params) do
{
file_path: file_path,
@@ -14,9 +17,9 @@ describe Files::UpdateService do
file_content: new_contents,
file_content_encoding: "text",
last_commit_sha: last_commit_sha,
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
+ start_project: project,
+ start_branch: project.default_branch,
+ target_branch: target_branch
}
end
@@ -54,18 +57,6 @@ describe Files::UpdateService do
end
context "when the last_commit_sha is not supplied" do
- let(:commit_params) do
- {
- file_path: file_path,
- commit_message: "Update File",
- file_content: new_contents,
- file_content_encoding: "text",
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
- }
- end
-
it "returns a hash with the :success status " do
results = subject.execute
@@ -80,5 +71,15 @@ describe Files::UpdateService do
expect(results.data).to eq(new_contents)
end
end
+
+ context 'when target branch is different than source branch' do
+ let(:target_branch) { "#{project.default_branch}-new" }
+
+ it 'fires hooks only once' do
+ expect(GitHooksService).to receive(:new).once.and_call_original
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 41b0968b8b4..3318dfb22b6 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -21,7 +21,7 @@ describe GitHooksService, services: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil])
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }
end
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
new file mode 100644
index 00000000000..98c560ffb26
--- /dev/null
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Groups::DestroyService, services: true do
+ include DatabaseConnectionHelpers
+
+ 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" }
+
+ before do
+ group.add_user(user, Gitlab::Access::OWNER)
+ end
+
+ shared_examples 'group destruction' do |async|
+ context 'database records' do
+ before do
+ destroy_group(group, user, async)
+ 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
+
+ context 'file system' do
+ context 'Sidekiq inline' do
+ before do
+ # Run sidekiq immediatly to check that renamed dir will be removed
+ Sidekiq::Testing.inline! { destroy_group(group, user, async) }
+ end
+
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
+ end
+
+ context 'Sidekiq fake' do
+ before do
+ # Don't run sidekiq to check if renamed repository exists
+ Sidekiq::Testing.fake! { destroy_group(group, user, async) }
+ end
+
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
+ end
+ end
+
+ def destroy_group(group, user, async)
+ if async
+ Groups::DestroyService.new(group, user).async_execute
+ else
+ Groups::DestroyService.new(group, user).execute
+ end
+ end
+ end
+
+ describe 'asynchronous delete' do
+ it_behaves_like 'group destruction', true
+
+ context 'potential race conditions' do
+ context "when the `GroupDestroyWorker` task runs immediately" do
+ it "deletes the group" do
+ # Commit the contents of this spec's transaction so far
+ # so subsequent db connections can see it.
+ #
+ # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
+ # transaction is currently in progress". Without this, this
+ # spec will always be green, since the group created in setup
+ # cannot be seen by any other connections / threads in this spec.
+ Group.connection.commit_db_transaction
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).not_to be_nil
+
+ # Execute the contents of `GroupDestroyWorker` in a separate thread, to
+ # simulate data manipulation by the Sidekiq worker (different database
+ # connection / transaction).
+ expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
+ Thread.new { m[group_id, user_id] }.join(5)
+ end
+
+ # Kick off the initial group destroy in a new thread, so that
+ # it doesn't share this spec's database transaction.
+ Thread.new { Groups::DestroyService.new(group, user).async_execute }.join(5)
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'synchronous delete' do
+ it_behaves_like 'group destruction', false
+ end
+
+ context 'projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save
+ end
+
+ it_behaves_like 'group destruction', false
+ end
+end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 531180e48a1..7c0fccb9d41 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -51,7 +51,7 @@ describe Groups::UpdateService, services: true do
end
context 'rename group' do
- let!(:service) { described_class.new(internal_group, user, path: 'new_path') }
+ let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 4cfba35c830..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
@@ -120,11 +130,20 @@ describe Issues::BuildService, services: true do
end
describe '#execute' do
+ let(:milestone) { create(:milestone, project: project) }
+
it 'builds a new issues with given params' do
- issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute
+ issue = described_class.new(
+ project,
+ user,
+ title: 'Issue #1',
+ description: 'Issue description',
+ milestone_id: milestone.id,
+ ).execute
expect(issue.title).to eq('Issue #1')
expect(issue.description).to eq('Issue description')
+ expect(issue.milestone).to eq(milestone)
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index ac3834c32ff..776cbc4296b 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
+ expect(issue.description).to eq('please fix')
expect(issue.assignee).to be_nil
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
@@ -139,46 +140,177 @@ 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 } }
+
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
+
+ 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
- expect(discussion.resolved?).to be(true)
+ 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
- it 'added a system note to the discussion' do
- described_class.new(project, user, opts).execute
+ describe 'for a merge request' do
+ let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
- reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
- expect(reloaded_discussion.last_note.system).to eq(true)
+ 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
- it 'assigns the title and description for the issue' do
- issue = described_class.new(project, user, opts).execute
+ context 'checking spam' do
+ let(:opts) do
+ {
+ title: 'Awesome issue',
+ description: 'please fix',
+ request: double(:request, env: {})
+ }
+ end
+
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when recaptcha was verified' do
+ let(:log_user) { user }
+ let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: 'Awesome issue') }
+
+ before do
+ opts[:recaptcha_verified] = true
+ opts[:spam_log_id] = spam_logs.last.id
+
+ expect(AkismetService).not_to receive(:new)
+ end
+
+ it 'does no mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+
+ it 'marks related spam_log as recaptcha_verified' do
+ expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true)
+ end
+
+ context 'when spam log does not belong to a user' do
+ let(:log_user) { create(:user) }
- expect(issue.title).not_to be_nil
- expect(issue.description).not_to be_nil
+ it 'does not mark spam_log as recaptcha_verified' do
+ expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ end
+ 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
+ context 'when recaptcha was not verified' do
+ context 'when akismet detects spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ it 'marks an issue as a spam ' do
+ expect(issue).to be_spam
+ end
+
+ it 'an issue is not valid ' do
+ expect(issue.valid?).to be_falsey
+ end
- expect(issue.description).to be_nil
- expect(issue.title).to be_nil
+ it 'creates a new spam_log' do
+ expect{issue}.to change{SpamLog.count}.from(0).to(1)
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(issue.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ context 'when akismet does not detect spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
+
+ it 'does not mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+ end
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/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index bb7830c7eea..d80fb8a1af1 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
- let(:todo_service) { TodoService.new }
+ let(:todo_service) { spy('todo service') }
let(:merge_request) do
create(:merge_request, merge_user: user,
@@ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
end
end
end
+
+ describe '#close_all' do
+ context 'when using pipeline that belongs to merge request' do
+ it 'resolves todos about failed builds for pipeline' do
+ service.close_all(pipeline)
+
+ expect(todo_service)
+ .to have_received(:merge_request_build_retried)
+ .with(merge_request)
+ end
+ end
+
+ context 'when pipeline is not related to merge request' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'does not resolve any todos about failed builds' do
+ service.close_all(pipeline)
+
+ expect(todo_service)
+ .not_to have_received(:merge_request_build_retried)
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index dc945ca4868..0768f644036 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -44,15 +44,14 @@ describe MergeRequests::BuildService, services: true do
end
end
- context 'missing target branch' do
- let(:target_branch) { '' }
+ context 'when target branch is missing' do
+ let(:target_branch) { nil }
+ let(:commits) { Commit.decorate([commit_1], project) }
- it 'forbids the merge request from being created' do
+ it 'creates compare object with target branch as default branch' do
expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('You must select source and target branch')
+ expect(merge_request.compare).to be_present
+ expect(merge_request.target_branch).to eq(project.default_branch)
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 5a89acc96a4..d96f819e66a 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do
context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
- it 'saves error if there is an exception' do
- allow(service).to receive(:repository).and_raise("error message")
+ before do
+ allow(Rails.logger).to receive(:error)
+ end
+ it 'logs and saves error if there is an exception' do
+ error_message = 'error message'
+
+ allow(service).to receive(:repository).and_raise("error message")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("Something went wrong during merge: error message")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- it 'saves error if there is an PreReceiveError exception' do
- allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error")
+ it 'logs and saves error if there is an PreReceiveError exception' do
+ error_message = 'error message'
+ allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, error_message)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("error")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- it 'aborts if there is a merge conflict' do
+ it 'logs and saves error if there is a merge conflict' do
+ error_message = 'Conflicts detected during merge'
+
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.open?).to be_truthy
+ expect(merge_request).to be_open
expect(merge_request.merge_commit_sha).to be_nil
- expect(merge_request.merge_error).to eq("Conflicts detected during merge")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
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 314ea670a71..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
@@ -89,19 +91,21 @@ describe MergeRequests::RefreshService, services: true do
# Merge master -> feature branch
author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
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
@@ -287,41 +297,64 @@ describe MergeRequests::RefreshService, services: true do
it 'references the commit that caused the Work in Progress status' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
-
allow(refresh_service).to receive(:find_new_commits)
refresh_service.instance_variable_set("@commits", [
- instance_double(
- Commit,
+ double(
id: 'aaaaaaa',
+ sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
short_id: 'aaaaaaa',
title: 'Fix issue',
work_in_progress?: false
),
- instance_double(
- Commit,
+ double(
id: 'bbbbbbb',
+ sha: '498214de67004b1da3d820901307bed2a68a8ef6',
short_id: 'bbbbbbb',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'bbbbbbb'
),
- instance_double(
- Commit,
+ double(
id: 'ccccccc',
+ sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141',
short_id: 'ccccccc',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'ccccccc'
),
])
-
refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
reload_mrs
-
expect(@merge_request.notes.last.note).to eq(
"marked as a **Work In Progress** from bbbbbbb"
)
end
+
+ it 'does not mark as WIP based on commits that do not belong to an MR' do
+ allow(refresh_service).to receive(:find_new_commits)
+ refresh_service.instance_variable_set("@commits", [
+ double(
+ id: 'aaaaaaa',
+ sha: 'aaaaaaa',
+ short_id: 'aaaaaaa',
+ title: 'Fix issue',
+ work_in_progress?: false
+ ),
+ double(
+ id: 'bbbbbbb',
+ sha: 'bbbbbbbb',
+ short_id: 'bbbbbbb',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'bbbbbbb'
+ )
+ ])
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+
+ expect(@merge_request.work_in_progress?).to be_falsey
+ end
end
def reload_mrs
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index 388abb6a0df..d33535d22af 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -59,14 +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(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+ project.repository.create_file(
+ user,
+ 'new-file-in-target',
+ '',
+ message: 'Add new file in target',
+ branch_name: 'conflict-start')
end
before do
@@ -119,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/notes/delete_service_spec.rb b/spec/services/notes/delete_service_spec.rb
deleted file mode 100644
index 1d0a747a480..00000000000
--- a/spec/services/notes/delete_service_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'spec_helper'
-
-describe Notes::DeleteService, services: true do
- describe '#execute' do
- it 'deletes a note' do
- project = create(:empty_project)
- issue = create(:issue, project: project)
- note = create(:note, project: project, noteable: issue)
-
- described_class.new(project, note.author).execute(note)
-
- expect(project.issues.find(issue.id).notes).not_to include(note)
- end
- end
-end
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
new file mode 100644
index 00000000000..f53f96e0c2b
--- /dev/null
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Notes::DestroyService, services: true do
+ describe '#execute' do
+ it 'deletes a note' do
+ project = create(:empty_project)
+ issue = create(:issue, project: project)
+ note = create(:note, project: project, noteable: issue)
+
+ described_class.new(project, note.author).execute(note)
+
+ expect(project.issues.find(issue.id).notes).not_to include(note)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 7cf2cd9968f..ebbaea4e59a 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -146,6 +146,16 @@ 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)
@@ -476,6 +486,20 @@ 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)
@@ -665,6 +689,19 @@ 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)
@@ -818,6 +855,20 @@ 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)
@@ -999,20 +1050,28 @@ 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 the pipeline succeeds is false but they've opted into notifications about their activity" do
+ merge_request.merge_when_pipeline_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 }
diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb
new file mode 100644
index 00000000000..aa63fe3a5c1
--- /dev/null
+++ b/spec/services/pages_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe PagesService, services: true do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:service) { PagesService.new(data) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ context 'execute asynchronously for pages job' do
+ before { build.name = 'pages' }
+
+ context 'on success' do
+ before { build.success }
+
+ it 'executes worker' do
+ expect(PagesWorker).to receive(:perform_async)
+ service.execute
+ end
+ end
+
+ %w(pending running failed canceled).each do |status|
+ context "on #{status}" do
+ before { build.status = status }
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+ end
+ end
+
+ context 'for other jobs' do
+ before do
+ build.name = 'other job'
+ build.success
+ end
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index a1539b69401..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)
@@ -90,10 +90,6 @@ describe Projects::CreateService, '#execute', services: true do
end
context 'global builds_enabled true does enable CI by default' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
- end
-
it { is_expected.to be_truthy }
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 90771825f5c..74bfba44dfd 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -9,12 +9,27 @@ describe Projects::DestroyService, services: true do
shared_examples 'deleting the project' do
it 'deletes the project' do
- expect(Project.all).not_to include(project)
+ expect(Project.unscoped.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
expect(Dir.exist?(remove_path)).to be_falsey
end
end
+ shared_examples 'deleting the project with pipeline and build' do
+ context 'with pipeline and build' do # which has optimistic locking
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ perform_enqueued_jobs do
+ destroy_project(project, user, {})
+ end
+ end
+
+ it_behaves_like 'deleting the project'
+ end
+ end
+
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed repository will be removed
@@ -35,30 +50,43 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
- context 'async delete of project with private issue visibility' do
- let!(:async) { true }
-
+ context 'when flushing caches fail' do
before do
- project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
- # Run sidekiq immediately to check that renamed repository will be removed
- Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ new_user = create(:user)
+ project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
+ allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError)
end
- it_behaves_like 'deleting the project'
- end
+ it 'keeps project team intact upon an error' do
+ Sidekiq::Testing.inline! do
+ begin
+ destroy_project(project, user, {})
+ rescue Redis::CannotConnectError
+ end
+ end
- context 'delete with pipeline' do # which has optimistic locking
- let!(:pipeline) { create(:ci_pipeline, project: project) }
+ expect(project.team.members.count).to eq 1
+ end
+ end
- before do
- expect(project).to receive(:destroy!).and_call_original
+ context 'with async_execute' do
+ let(:async) { true }
- perform_enqueued_jobs do
- destroy_project(project, user, {})
+ context 'async delete of project with private issue visibility' do
+ before do
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ # Run sidekiq immediately to check that renamed repository will be removed
+ Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
+
+ it_behaves_like 'deleting the project'
end
- it_behaves_like 'deleting the project'
+ it_behaves_like 'deleting the project with pipeline and build'
+ end
+
+ context 'with execute' do
+ it_behaves_like 'deleting the project with pipeline and build'
end
context 'container registry' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1540b90163a..5c6fbea8d0e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do
before do
allow_any_instance_of(Gitlab::UploadsTransfer).
to receive(:move_project).and_return(true)
+ allow_any_instance_of(Gitlab::PagesTransfer).
+ to receive(:move_project).and_return(true)
group.add_owner(user)
@result = transfer_project(project, user, group)
end
@@ -81,4 +83,30 @@ describe Projects::TransferService, services: true do
transfer_project(project, user, group)
end
end
+
+ describe 'refreshing project authorizations' do
+ let(:group) { create(:group) }
+ let(:owner) { project.namespace.owner }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_user(owner, GroupMember::MASTER)
+ group.add_user(group_member, GroupMember::DEVELOPER)
+ end
+
+ it 'refreshes the permissions of the old and new namespace' do
+ transfer_project(project, owner, group)
+
+ expect(group_member.authorized_projects).to include(project)
+ expect(owner.authorized_projects).to include(project)
+ end
+
+ it 'only schedules a single job for every user' do
+ expect(UserProjectAccessChangedService).to receive(:new).
+ with([owner.id, group_member.id]).
+ and_call_original
+
+ transfer_project(project, owner, group)
+ end
+ end
end
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
new file mode 100644
index 00000000000..8b329bc21c3
--- /dev/null
+++ b/spec/services/projects/update_pages_configuration_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Projects::UpdatePagesConfigurationService, services: true do
+ let(:project) { create(:empty_project) }
+ subject { described_class.new(project) }
+
+ describe "#update" do
+ let(:file) { Tempfile.new('pages-test') }
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ it 'updates the .update file' do
+ # Access this reference to ensure scoping works
+ Projects::Settings # rubocop:disable Lint/Void
+ expect(subject).to receive(:pages_config_file).and_return(file.path)
+ expect(subject).to receive(:reload_daemon).and_call_original
+
+ expect(subject.execute).to eq({ status: :success })
+ end
+ end
+end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
new file mode 100644
index 00000000000..f75fdd9e03f
--- /dev/null
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -0,0 +1,102 @@
+require "spec_helper"
+
+describe Projects::UpdatePagesService do
+ let(:project) { create :project }
+ let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha }
+ let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' }
+ let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+
+ subject { described_class.new(project, build) }
+
+ before do
+ project.remove_pages
+ end
+
+ %w(tar.gz zip).each do |format|
+ context "for valid #{format}" do
+ let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") }
+ let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") }
+ let(:metadata) do
+ filename = Rails.root + "spec/fixtures/pages.#{format}.meta"
+ fixture_file_upload(filename) if File.exist?(filename)
+ end
+
+ before do
+ build.update_attributes(artifacts_file: file)
+ 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)
+ expect(project.pages_deployed?).to be_truthy
+ end
+
+ it 'limits pages size' do
+ stub_application_setting(max_pages_size: 1)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'removes pages after destroy' do
+ expect(PagesWorker).to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ project.destroy
+ expect(project.pages_deployed?).to be_falsey
+ end
+
+ it 'fails if sha on branch is not latest' do
+ pipeline.update_attributes(sha: 'old_sha')
+ build.update_attributes(artifacts_file: file)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for empty file fails' do
+ build.update_attributes(artifacts_file: empty_file)
+ expect(execute).not_to eq(:success)
+ end
+ end
+ end
+
+ it 'fails to remove project pages when no pages is deployed' do
+ expect(PagesWorker).not_to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ project.destroy
+ end
+
+ it 'fails if no artifacts' do
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for invalid archive' do
+ build.update_attributes(artifacts_file: invalid_file)
+ expect(execute).not_to eq(:success)
+ end
+
+ def execute
+ subject.execute[:status]
+ end
+end
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 66fc8fc360b..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 }
@@ -653,5 +661,68 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
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) }
+ let(:service) { described_class.new(non_empty_project, developer)}
+
+ it 'updates target_branch if /target_branch command is executed' do
+ _, updates = service.execute('/target_branch merge-test', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ it 'handles blanks around param' do
+ _, updates = service.execute('/target_branch merge-test ', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ context 'ignores command with no argument' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+
+ context 'ignores non-existing target branch' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch totally_non_existing_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+ end
end
end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
new file mode 100644
index 00000000000..4ce3b95aa87
--- /dev/null
+++ b/spec/services/spam_service_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe SpamService, services: true do
+ describe '#when_recaptcha_verified' do
+ def check_spam(issue, request, recaptcha_verified)
+ described_class.new(issue, request).when_recaptcha_verified(recaptcha_verified) do
+ 'yielded'
+ end
+ end
+
+ it 'yields block when recaptcha was already verified' do
+ issue = build_stubbed(:issue)
+
+ expect(check_spam(issue, nil, true)).to eql('yielded')
+ end
+
+ context 'when recaptcha was not verified' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:request) { double(:request, env: {}) }
+
+ context 'when indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) }
+
+ it 'doesnt check as spam when request is missing' do
+ check_spam(issue, nil, false)
+
+ expect(issue.spam).to be_falsey
+ end
+
+ it 'checks as spam' do
+ check_spam(issue, request, false)
+
+ expect(issue.spam).to be_truthy
+ end
+
+ it 'creates a spam log' do
+ expect { check_spam(issue, request, false) }
+ .to change { SpamLog.count }.from(0).to(1)
+ end
+
+ it 'doesnt yield block' do
+ expect(check_spam(issue, request, false))
+ .to eql(SpamLog.last)
+ end
+ end
+
+ context 'when not indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) }
+
+ it 'returns false' do
+ expect(check_spam(issue, request, false)).to be_falsey
+ end
+
+ it 'does not create a spam log' do
+ expect { check_spam(issue, request, false) }
+ .not_to change { SpamLog.count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index bd7269045e1..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'
@@ -592,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
@@ -752,13 +752,13 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h"
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
end
context 'without a time estimate' do
it 'sets the note text' do
- expect(subject.note).to eq "Removed time estimate on this issue"
+ expect(subject.note).to eq "removed time estimate"
end
end
end
@@ -782,7 +782,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(277200)
- expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request"
+ expect(subject.note).to eq "added 1w 4d 5h of time spent"
end
end
@@ -790,7 +790,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(-277200)
- expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request"
+ expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
end
end
@@ -798,7 +798,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(:reset)
- expect(subject.note).to eq "Removed time spent on this merge request"
+ expect(subject.note).to eq "removed time spent"
end
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
new file mode 100644
index 00000000000..5478b8c9ec0
--- /dev/null
+++ b/spec/services/tags/create_service_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Tags::CreateService, services: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ it 'creates the tag and returns success' do
+ response = service.execute('v42.42.42', 'master', 'Foo')
+
+ expect(response[:status]).to eq(:success)
+ expect(response[:tag]).to be_a Gitlab::Git::Tag
+ expect(response[:tag].name).to eq('v42.42.42')
+ end
+
+ context 'when target is invalid' do
+ it 'returns an error' do
+ response = service.execute('v1.1.0', 'foo', 'Foo')
+
+ expect(response).to eq(status: :error,
+ message: 'Target foo is invalid')
+ end
+ end
+
+ context 'when tag already exists' do
+ it 'returns an error' do
+ expect(repository).to receive(:add_tag).
+ with(user, 'v1.1.0', 'master', 'Foo').
+ and_raise(Rugged::TagError)
+
+ response = service.execute('v1.1.0', 'master', 'Foo')
+
+ expect(response).to eq(status: :error,
+ message: 'Tag v1.1.0 already exists')
+ end
+ end
+
+ context 'when pre-receive hook fails' do
+ it 'returns an error' do
+ expect(repository).to receive(:add_tag).
+ with(user, 'v1.1.0', 'master', 'Foo').
+ and_raise(GitHooksService::PreReceiveError, 'something went wrong')
+
+ response = service.execute('v1.1.0', 'master', 'Foo')
+
+ expect(response).to eq(status: :error,
+ message: 'something went wrong')
+ end
+ end
+ end
+end
diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb
new file mode 100644
index 00000000000..a388c93379a
--- /dev/null
+++ b/spec/services/tags/destroy_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Tags::DestroyService, services: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ it 'removes the tag' do
+ expect(repository).to receive(:before_remove_tag)
+ expect(service).to receive(:success)
+
+ service.execute('v1.1.0')
+ end
+ end
+end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 13d584a8975..a8395cb48ea 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -9,7 +9,9 @@ describe TodoService, services: true do
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
@@ -21,8 +23,10 @@ describe TodoService, services: true do
describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
+ let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) }
describe '#new_issue' do
it 'creates a todo if assigned' do
@@ -52,6 +56,26 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_issue(addressed_issue, author)
+
+ should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
+ it 'creates correct todos for each valid user based on the type of mention' do
+ issue.update(description: directly_addressed_and_mentioned)
+
+ service.new_issue(issue, author)
+
+ should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
+ end
+
it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
@@ -63,6 +87,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo if user cannot see the issue when issue is confidential' do
+ service.new_issue(addressed_confident_issue, john_doe)
+
+ should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::ASSIGNED)
+ should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
context 'when a private group is mentioned' do
let(:group) { create :group, :private }
let(:project) { create :project, :private, group: group }
@@ -94,12 +129,38 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ issue.update(description: directly_addressed_and_mentioned)
+
+ service.update_issue(issue, author)
+
+ should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.update_issue(addressed_issue, author)
+
+ should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
+ it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author)
+
+ expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count)
+ end
+
it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
@@ -111,6 +172,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+ it 'does not create a directly addressed todo if user can not see the issue when issue is confidential' do
+ service.update_issue(addressed_confident_issue, john_doe)
+
+ should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
context 'issues with a task list' do
it 'does not create todo when tasks are marked as completed' do
issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
@@ -125,6 +197,19 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo when tasks are marked as completed' do
+ addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
+
+ service.update_issue(addressed_issue, author)
+
+ should_not_create_todo(user: admin, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: assignee, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not raise an error when description not change' do
issue.update(title: 'Sample')
@@ -202,39 +287,51 @@ describe TodoService, services: true do
end
end
- shared_examples 'marking todos as done' do |meth|
- let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ shared_examples 'updating todos state' do |meth, state, new_state|
+ let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
- it 'marks related todos for the user as done' do
+ it 'updates related todos for the user with the new_state' do
service.send(meth, collection, john_doe)
- expect(first_todo.reload).to be_done
- expect(second_todo.reload).to be_done
+ expect(first_todo.reload.state?(new_state)).to be true
+ expect(second_todo.reload.state?(new_state)).to be true
end
describe 'cached counts' do
it 'updates when todos change' do
- expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(2)
+ expect(john_doe.todos.where(state: new_state).count).to eq(0)
+ expect(john_doe.todos.where(state: state).count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(2)
- expect(john_doe.todos_pending_count).to eq(0)
+ expect(john_doe.todos.where(state: new_state).count).to eq(2)
+ expect(john_doe.todos.where(state: state).count).to eq(0)
end
end
end
describe '#mark_todos_as_done' do
- it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do
let(:collection) { [first_todo, second_todo] }
end
end
describe '#mark_todos_as_done_by_ids' do
- it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
+ describe '#mark_todos_as_pending' do
+ it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_pending_by_ids' do
+ it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do
let(:collection) { [first_todo, second_todo].map(&:id) }
end
end
@@ -244,8 +341,11 @@ describe TodoService, services: true do
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
+ let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
+ let(:addressed_note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: directly_addressed) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
+ let(:addressed_note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: directly_addressed) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
@@ -276,6 +376,26 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ note.update(note: directly_addressed_and_mentioned)
+
+ service.new_note(note, john_doe)
+
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: note)
+ should_create_todo(user: admin, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_note(addressed_note, john_doe)
+
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: author, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ end
+
it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
@@ -287,6 +407,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
+ it 'does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue' do
+ service.new_note(addressed_note_on_confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ end
+
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
service.new_note(note_on_commit, john_doe)
@@ -296,6 +427,15 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
+ it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(addressed_note_on_commit, john_doe)
+
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ end
+
it 'does not create todo when leaving a note on snippet' do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
@@ -324,6 +464,7 @@ describe TodoService, services: true do
describe 'Merge Requests' do
let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
describe '#new_merge_request' do
@@ -350,6 +491,25 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'creates a todo for each valid user based on the type of mention' do
+ mr_assigned.update(description: directly_addressed_and_mentioned)
+
+ service.new_merge_request(mr_assigned, author)
+
+ should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_merge_request(addressed_mr_assigned, author)
+
+ should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#update_merge_request' do
@@ -363,12 +523,37 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ mr_assigned.update(description: directly_addressed_and_mentioned)
+
+ service.update_merge_request(mr_assigned, author)
+
+ should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.update_merge_request(addressed_mr_assigned, author)
+
+ should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
+ it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author)
+
+ expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
+ end
+
context 'with a task list' do
it 'does not create todo when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
@@ -384,6 +569,20 @@ describe TodoService, services: true do
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo when tasks are marked as completed' do
+ addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
+
+ service.update_merge_request(addressed_mr_assigned, author)
+
+ should_not_create_todo(user: admin, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: assignee, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not raise an error when description not change' do
mr_assigned.update(title: 'Sample')
@@ -436,6 +635,11 @@ describe TodoService, services: true do
service.reassigned_merge_request(mr_assigned, author)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'does not create a directly addressed todo for guests' do
+ service.reassigned_merge_request(addressed_mr_assigned, author)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#merge_merge_request' do
@@ -452,6 +656,11 @@ describe TodoService, services: true do
service.merge_merge_request(mr_assigned, john_doe)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'does not create directly addressed todo for guests' do
+ service.merge_merge_request(addressed_mr_assigned, john_doe)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#new_award_emoji' do
@@ -471,7 +680,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)
@@ -491,7 +700,7 @@ 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)
@@ -509,6 +718,7 @@ describe TodoService, services: true do
describe '#new_note' do
let(:mention) { john_doe.to_reference }
let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+ let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") }
let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
it 'creates a todo for mentioned user on new diff note' do
@@ -517,6 +727,12 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request)
end
+ it 'creates a directly addressed todo for addressed user on new diff note' do
+ service.new_note(addressed_diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::DIRECTLY_ADDRESSED, note: addressed_diff_note_on_merge_request)
+ end
+
it 'creates a todo for mentioned user on legacy diff note' do
service.new_note(legacy_diff_note_on_merge_request, author)
@@ -536,7 +752,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)
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
new file mode 100644
index 00000000000..922e82445d0
--- /dev/null
+++ b/spec/services/users/destroy_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Users::DestroyService, services: true do
+ describe "Deletes a user and all their personal projects" do
+ let!(:user) { create(:user) }
+ let!(:admin) { create(:admin) }
+ let!(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace) }
+ let(:service) { described_class.new(admin) }
+
+ context 'no options are given' do
+ it 'deletes the user' do
+ user_data = service.execute(user)
+
+ expect { user_data['email'].to eq(user.email) }
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'will delete the project in the near future' do
+ expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
+
+ service.execute(user)
+ 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) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ service.execute(user)
+ end
+
+ it 'does not delete the user' do
+ expect(User.find(user.id)).to eq user
+ end
+ end
+
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ service.execute(user, delete_solo_owned_groups: true)
+ end
+
+ it 'deletes solo owned groups' do
+ expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'deletes the user' do
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context "deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
+
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect(User.exists?(user.id)).to be(true)
+ end
+
+ it 'allows admins to delete anyone' do
+ described_class.new(admin).execute(user)
+
+ expect(User.exists?(user.id)).to be(false)
+ end
+
+ it 'allows users to delete their own account' do
+ described_class.new(user).execute(user)
+
+ expect(User.exists?(user.id)).to be(false)
+ end
+ end
+ end
+end
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/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
new file mode 100644
index 00000000000..5341ba3d261
--- /dev/null
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe WikiPages::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:opts) do
+ {
+ title: 'Title',
+ content: 'Content for wiki page',
+ format: 'markdown'
+ }
+ end
+ let(:service) { described_class.new(project, user, opts) }
+
+ describe '#execute' do
+ context "valid params" do
+ before do
+ allow(service).to receive(:execute_hooks)
+ project.add_master(user)
+ end
+
+ subject { service.execute }
+
+ it 'creates a valid wiki page' do
+ is_expected.to be_valid
+ expect(subject.title).to eq(opts[:title])
+ expect(subject.content).to eq(opts[:content])
+ expect(subject.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to have_received(:execute_hooks).once.with(subject, 'create')
+ end
+ end
+ end
+end
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
new file mode 100644
index 00000000000..a4b9a390fe2
--- /dev/null
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe WikiPages::DestroyService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:wiki_page) { create(:wiki_page) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:execute_hooks)
+ project.add_master(user)
+ end
+
+ it 'executes webhooks' do
+ service.execute(wiki_page)
+
+ expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete')
+ end
+ end
+end
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
new file mode 100644
index 00000000000..2bccca764d7
--- /dev/null
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe WikiPages::UpdateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:wiki_page) { create(:wiki_page) }
+ let(:opts) do
+ {
+ content: 'New content for wiki page',
+ format: 'markdown',
+ message: 'New wiki message'
+ }
+ end
+ let(:service) { described_class.new(project, user, opts) }
+
+ describe '#execute' do
+ context "valid params" do
+ before do
+ allow(service).to receive(:execute_hooks)
+ project.add_master(user)
+ end
+
+ subject { service.execute(wiki_page) }
+
+ it 'updates the wiki page' do
+ is_expected.to be_valid
+ expect(subject.content).to eq(opts[:content])
+ expect(subject.format).to eq(opts[:format].to_sym)
+ expect(subject.message).to eq(opts[:message])
+ end
+
+ it 'executes webhooks' do
+ expect(service).to have_received(:execute_hooks).once.with(subject, 'update')
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ab38dac65c5..ceb3209331f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -35,6 +35,7 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitForAjax, type: :feature
config.include StubConfiguration
config.include EmailHelpers, type: :mailer
config.include TestEnv
@@ -42,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/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb
deleted file mode 100644
index 352a6eeec79..00000000000
--- a/spec/support/api/pagination_shared_examples.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# Specs for paginated resources.
-#
-# Requires an API request:
-# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
-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
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 68b196d9033..35d1e1cfc7d 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -17,8 +17,8 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
- def api(path, user = nil)
- "/api/#{API::API.version}#{path}" +
+ def api(path, user = nil, version: API::API.version)
+ "/api/#{version}#{path}" +
# Normalize query string
(path.index('?') ? '' : '?') +
@@ -31,6 +31,11 @@ module ApiHelpers
end
end
+ # Temporary helper method for simplifying V3 exclusive API specs
+ def v3_api(path, user = nil)
+ api(path, user, version: 'v3')
+ end
+
def ci_api(path, user = nil)
"/ci/api/v1/#{path}" +
@@ -44,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..62740ec29fd 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -3,7 +3,7 @@ require 'capybara/rspec'
require 'capybara/poltergeist'
# 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|
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 75c95d70951..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,7 +28,12 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ sha = project.repository.create_file(
+ user,
+ random_git_name,
+ 'content',
+ message: 'commit message',
+ branch_name: source_branch)
project.repository.commit(sha)
opts = {
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 247f0954221..6f31828b825 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -3,6 +3,10 @@ RSpec.configure do |config|
DatabaseCleaner.clean_with(:truncation)
end
+ config.append_after(:context) do
+ DatabaseCleaner.clean_with(:truncation)
+ end
+
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
diff --git a/spec/support/drag_to_helper.rb b/spec/support/drag_to_helper.rb
new file mode 100644
index 00000000000..0c0659d3ecd
--- /dev/null
+++ b/spec/support/drag_to_helper.rb
@@ -0,0 +1,13 @@
+module DragTo
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
+ evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until drag_active?
+ end
+ end
+
+ def drag_active?
+ page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ end
+end
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
new file mode 100644
index 00000000000..6b009b132b6
--- /dev/null
+++ b/spec/support/filtered_search_helpers.rb
@@ -0,0 +1,74 @@
+module FilteredSearchHelpers
+ def filtered_search
+ page.find('.filtered-search')
+ end
+
+ # 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)
+ # Add an extra space to engage visual tokens
+ filtered_search.send_keys("#{search_term} ")
+ filtered_search.send_keys(:enter)
+ end
+
+ def expect_filtered_search_input(input)
+ expect(find('.filtered-search').value).to eq(input)
+ end
+
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
+ end
+
+ def reset_filters
+ clear_search_field
+ filtered_search.send_keys(:enter)
+ end
+
+ def init_label_search
+ filtered_search.set('label:')
+ # 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/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index ce8dfe5ae75..cd55d63125e 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "theme_id":2,"color_scheme_id":2,
+ "color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+}
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index ce8dfe5ae75..cd55d63125e 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "theme_id":2,"color_scheme_id":2,
+ "color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+}
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 1b0a4583f5c..944ea30656f 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -35,7 +35,7 @@ module ExportFileHelper
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
new file mode 100644
index 00000000000..4c0f556e736
--- /dev/null
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -0,0 +1,36 @@
+shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
+ before do
+ @issuable_ids = []
+
+ 2.times do
+ 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
+
+ issuable.id.times { create(:note, noteable: issuable, project: issuable.project) }
+ (issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) }
+ (issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) }
+ end
+ end
+
+ it "creates indexed meta-data object for issuable notes and votes count" do
+ if action
+ get action
+ else
+ get :index, namespace_id: project.namespace, project_id: project
+ end
+
+ meta_data = assigns(:issuable_meta_data)
+
+ @issuable_ids.each do |id|
+ expect(meta_data[id].notes_count).to eq(id)
+ expect(meta_data[id].downvotes).to eq(id + 1)
+ expect(meta_data[id].upvotes).to eq(id + 2)
+ end
+ end
+end
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 6c4c246a68b..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
@@ -43,7 +44,8 @@ module KubernetesHelpers
url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
- created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
+ max_session_time: 0
}
terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
terminal
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/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/matchers/match_file.rb b/spec/support/matchers/match_file.rb
new file mode 100644
index 00000000000..d1888b3376a
--- /dev/null
+++ b/spec/support/matchers/match_file.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :match_file do |expected|
+ match do |actual|
+ expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected)))
+ end
+end
diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb
new file mode 100644
index 00000000000..60f5e8239a7
--- /dev/null
+++ b/spec/support/matchers/pagination_matcher.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :include_pagination_headers do |expected|
+ match do |actual|
+ expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link')
+ end
+end
diff --git a/spec/support/matchers/satisfy_matchers.rb b/spec/support/matchers/satisfy_matchers.rb
new file mode 100644
index 00000000000..585915bac93
--- /dev/null
+++ b/spec/support/matchers/satisfy_matchers.rb
@@ -0,0 +1,19 @@
+# These matchers are a syntactic hack to provide more readable expectations for
+# an Enumerable object.
+#
+# They take advantage of the `all?`, `none?`, and `one?` methods, and the fact
+# that RSpec provides a `be_something` matcher for all predicates.
+#
+# Example:
+#
+# # Ensure exactly one object in an Array satisfies a condition
+# expect(users.one? { |u| u.admin? }).to eq true
+#
+# # The same thing, but using the `be_one` matcher
+# expect(users).to be_one { |u| u.admin? }
+#
+# # The same thing again, but using `satisfy_one` for improved readability
+# expect(users).to satisfy_one { |u| u.admin? }
+RSpec::Matchers.alias_matcher :satisfy_all, :be_all
+RSpec::Matchers.alias_matcher :satisfy_none, :be_none
+RSpec::Matchers.alias_matcher :satisfy_one, :be_one
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb
index d5801c8272f..326b85eabd0 100644
--- a/spec/support/merge_request_helpers.rb
+++ b/spec/support/merge_request_helpers.rb
@@ -10,4 +10,13 @@ module MergeRequestHelpers
def last_merge_request
page.all('ul.mr-list > li').last.text
end
+
+ def expect_mr_list_count(open_count, closed_count = 0)
+ all_count = open_count + closed_count
+
+ expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: open_count)
+ end
+ 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..07f81e9c4f3 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_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze
def ensure_seeds
if File.exist?(SEED_REPOSITORY_PATH)
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_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
index 93c0267d2db..4f0c745b7ee 100644
--- a/spec/support/services/issuable_create_service_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_shared_examples.rb
@@ -31,8 +31,8 @@ shared_examples 'issuable create service' do
context "when issuable feature is private" do
before do
- project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
- project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
end
levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
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 dd54b0addda..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
@@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do
let(:example_params) do
{
assignee: create(:user),
- milestone_id: double(:milestone),
+ milestone_id: 1,
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
}
end
diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb
index df483afa0e3..0d91fe5fd5d 100644
--- a/spec/support/slash_commands_helpers.rb
+++ b/spec/support/slash_commands_helpers.rb
@@ -3,7 +3,7 @@ module SlashCommandsHelpers
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
- click_button 'Comment'
+ find('.comment-btn').trigger('click')
end
end
end
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 90f1a9c8798..f1d226b6ae3 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -36,8 +36,9 @@ module TestEnv
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
'deleted-image-test' => '6c17798',
- 'wip' => 'b9238ee'
- }
+ '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.
@@ -47,7 +48,7 @@ module TestEnv
'master' => '5937ac0',
'remove-submodule' => '2a33e0c',
'conflict-resolvable-fork' => '404fa3f'
- }
+ }.freeze
# Test environment
#
@@ -134,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
@@ -142,7 +143,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages.default
+ Gitlab.config.repositories.storages.default['path']
end
def backup_path
@@ -151,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/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 02657684b57..52f4fabdc47 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -77,6 +77,6 @@ end
def submit_time(slash_command)
fill_in 'note[note]', with: slash_command
- click_button 'Comment'
+ find('.comment-btn').trigger('click')
wait_for_ajax
end
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/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
new file mode 100644
index 00000000000..c32f9a740b7
--- /dev/null
+++ b/spec/tasks/config_lint_spec.rb
@@ -0,0 +1,27 @@
+require 'rake_helper'
+Rake.application.rake_require 'tasks/config_lint'
+
+describe ConfigLint do
+ let(:files){ ['lib/support/fake.sh'] }
+
+ it 'errors out if any bash scripts have errors' do
+ expect { ConfigLint.run(files){ system('exit 1') } }.to raise_error(SystemExit)
+ end
+
+ it 'passes if all scripts are fine' do
+ expect { ConfigLint.run(files){ system('exit 0') } }.not_to raise_error
+ end
+end
+
+describe 'config_lint rake task' do
+ before(:each) do
+ # Prevent `system` from actually being called
+ allow(Kernel).to receive(:system).and_return(true)
+ end
+
+ it 'runs lint on shell scripts' do
+ expect(Kernel).to receive(:system).with('bash', '-n', 'lib/support/init.d/gitlab')
+
+ run_rake_task('config_lint')
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bc751d20ce1..10458966cb9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
+ %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
@@ -107,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}" }
@@ -131,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" }
@@ -202,7 +184,7 @@ describe 'gitlab:app namespace rake task' do
it 'sets correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
@@ -210,14 +192,15 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('repositories/')
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
it 'deletes temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
)
expect(temp_dirs).to be_empty
@@ -244,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)
@@ -304,7 +287,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -312,6 +295,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -327,6 +311,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
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/teaspoon_env.rb b/spec/teaspoon_env.rb
deleted file mode 100644
index 643b161cdf4..00000000000
--- a/spec/teaspoon_env.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-Teaspoon.configure do |config|
- # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to
- # `http://localhost:3000/jasmine` to run your tests.
- config.mount_at = "/teaspoon"
-
- # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can
- # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`).
- # Note: Defaults to `Rails.root` if nil.
- config.root = nil
-
- # Paths that will be appended to the Rails assets paths
- # Note: Relative to `config.root`.
- config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"]
-
- # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will
- # be rendered as fixtures.
- config.fixture_paths = ["spec/javascripts/fixtures"]
-
- # SUITES
- #
- # You can modify the default suite configuration and create new suites here. Suites are isolated from one another.
- #
- # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can
- # omit various directives and the ones defined in the default suite will be used.
- #
- # To run a specific suite
- # - in the browser: http://localhost/teaspoon/[suite_name]
- # - with the rake task: rake teaspoon suite=[suite_name]
- # - with the cli: teaspoon --suite=[suite_name]
- config.suite do |suite|
- # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for
- # you -- which you can override with the directives below. This should be specified first, as it can override other
- # directives.
- # Note: If no version is specified, the latest is assumed.
- #
- # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0
- suite.use_framework :jasmine, "2.2.0"
-
- # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
- # files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
- suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
-
- # Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
- # suite.javascripts = []
-
- # You can include your own stylesheets if you want to change how Teaspoon looks.
- # Note: Spec related CSS can and should be loaded using fixtures.
- # suite.stylesheets = ["teaspoon"]
-
- # This suites spec helper, which can require additional support files. This file is loaded before any of your test
- # files are loaded.
- suite.helper = "spec_helper"
-
- # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating
- # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance.
- #
- # Available: boot, boot_require_js
- suite.boot_partial = "boot"
-
- # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure.
- suite.body_partial = "body"
-
- # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
- # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
- # suite.hook :fixtures, &proc{}
-
- # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated
- # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
- # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files.
- # suite.expand_assets = true
- end
-
- # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
- # be run in the default suite -- but can be focused into a more specific suite.
- # config.suite :targeted do |suite|
- # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
- # end
-
- # CONSOLE RUNNER SPECIFIC
- #
- # These configuration directives are applicable only when running via the rake task or command line interface. These
- # directives can be overridden using the command line interface arguments or with ENV variables when using the rake
- # task.
- #
- # Command Line Interface:
- # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js
- #
- # Rake:
- # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite
-
- # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver.
- #
- # Available: :phantomjs, :selenium, :capybara_webkit
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver = :phantomjs
-
- # Specify additional options for the driver.
- #
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver_options = nil
-
- # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
- # considered a failure. This is to avoid issues that can arise where tests stall.
- # config.driver_timeout = 180
-
- # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
- # config.server = nil
-
- # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
- # config.server_port = nil
-
- # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
- # want to lower this if you know it shouldn't take long to start.
- # config.server_timeout = 20
-
- # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
- # several suites, but in environments like CI this may not be desirable.
- # config.fail_fast = true
-
- # Specify the formatters to use when outputting the results.
- # Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
- #
- # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity
- # config.formatters = [:dot]
-
- # Specify if you want color output from the formatters.
- # config.color = true
-
- # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
- # remove them, but in verbose applications this may not be desirable.
- # config.suppress_log = false
-
- # COVERAGE REPORTS / THRESHOLD ASSERTIONS
- #
- # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and
- # display coverage statistics.
- #
- # Coverage configurations are similar to suites. You can define several, and use different ones under different
- # conditions.
- #
- # To run with a specific coverage configuration
- # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name]
- # - with the cli: teaspoon --coverage=[coverage_name]
-
- # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
- # on the CLI.
- # Set this to "true" or the name of your coverage config.
- config.use_coverage = true
-
- # You can have multiple coverage configs by passing a name to config.coverage.
- # e.g. config.coverage :ci do |coverage|
- # The default coverage config name is :default.
- config.coverage do |coverage|
- # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
- #
- # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- coverage.reports = ["text-summary", "html"]
-
- # The path that the coverage should be written to - when there's an artifact to write to disk.
- # Note: Relative to `config.root`.
- coverage.output_path = "coverage-javascript"
-
- # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
- # default excludes assets from vendor, gems and support libraries.
- coverage.ignore = [%r{vendor/}, %r{spec/javascripts/(?!helpers/)}]
-
- # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
- # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
- # coverage.statements = nil
- # coverage.functions = nil
- # coverage.branches = nil
- # coverage.lines = nil
- 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/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
new file mode 100644
index 00000000000..f5381a48207
--- /dev/null
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'projects/_home_panel', :view do
+ let(:project) { create(:empty_project, :public) }
+
+ let(:notification_settings) do
+ user&.notification_settings_for(project)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:notification_setting, notification_settings)
+
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:can?).and_return(false)
+ end
+
+ context 'when user is signed in' do
+ let(:user) { create(:user) }
+
+ it 'makes it possible to set notification level' do
+ render
+
+ expect(view).to render_template('shared/notifications/_button')
+ expect(rendered).to have_selector('.notification-dropdown')
+ end
+ end
+
+ context 'when user is signed out' do
+ let(:user) { nil }
+
+ it 'is not possible to set notification level' do
+ render
+
+ expect(rendered).not_to have_selector('.notification_dropdown')
+ end
+ 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 44870cfcfb3..ec78ac30593 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -15,7 +15,7 @@ describe 'projects/builds/show', :view do
allow(view).to receive(:can?).and_return(true)
end
- describe 'build information in header' do
+ describe 'job information in header' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
end
@@ -28,11 +28,11 @@ describe 'projects/builds/show', :view do
expect(rendered).to have_css('.ci-status.ci-success', text: 'passed')
end
- it 'does not render a link to the build' do
+ it 'does not render a link to the job' do
expect(rendered).not_to have_link('passed')
end
- it 'shows build id' do
+ it 'shows job id' do
expect(rendered).to have_css('.js-build-id', text: build.id)
end
@@ -45,8 +45,8 @@ describe 'projects/builds/show', :view do
end
end
- describe 'environment info in build view' do
- context 'build with latest deployment' do
+ describe 'environment info in job view' do
+ context 'job with latest deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
end
@@ -57,7 +57,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is the most recent deployment'
+ expected_text = 'This job is the most recent deployment'
render
expect(rendered).to have_css(
@@ -65,7 +65,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build with outdated deployment' do
+ context 'job with outdated deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
end
@@ -87,7 +87,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is an out-of-date deployment ' \
+ expected_text = 'This job is an out-of-date deployment ' \
"to staging.\nView the most recent deployment ##{second_deployment.iid}."
render
@@ -95,7 +95,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build failed to deploy' do
+ context 'job failed to deploy' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -105,7 +105,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed.'
+ expected_text = 'The deployment of this job to staging did not succeed.'
render
expect(rendered).to have_css(
@@ -113,7 +113,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build will deploy' do
+ context 'job will deploy' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -124,7 +124,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -137,7 +137,7 @@ describe 'projects/builds/show', :view do
end
it 'shows that deployment will be overwritten' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -150,7 +150,7 @@ describe 'projects/builds/show', :view do
context 'when environment does not exist' do
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -161,7 +161,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that failed to deploy and environment has not been created' do
+ context 'job that failed to deploy and environment has not been created' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -171,7 +171,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed'
+ expected_text = 'The deployment of this job to staging did not succeed'
render
expect(rendered).to have_css(
@@ -179,7 +179,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that will deploy and environment has not been created' do
+ context 'job that will deploy and environment has not been created' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -189,7 +189,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -200,7 +200,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'when build is running' do
+ context 'when job is running' do
before do
build.run!
render
@@ -209,9 +209,13 @@ 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 build is not running' do
+ context 'when job is not running' do
before do
build.success!
render
@@ -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..f2919f20e85 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
@@ -25,4 +27,30 @@ describe 'projects/commit/_commit_box.html.haml' do
expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} 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/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb
index b14b1ece2d0..b61f016967f 100644
--- a/spec/views/projects/notes/_form.html.haml_spec.rb
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -21,7 +21,7 @@ describe 'projects/notes/_form' do
let(:note) { build(:"note_on_#{noteable}", project: project) }
it 'says that only markdown is supported, not slash commands' do
- expect(rendered).to have_content('Styling with Markdown and slash commands are supported')
+ expect(rendered).to have_content('Markdown and slash commands are supported')
end
end
end
@@ -30,7 +30,7 @@ describe 'projects/notes/_form' do
let(:note) { build(:note_on_commit, project: project) }
it 'says that only markdown is supported, not slash commands' do
- expect(rendered).to have_content('Styling with Markdown is supported')
+ expect(rendered).to have_content('Markdown is supported')
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/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 14c56521280..0765573408c 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -5,14 +5,14 @@ describe DeleteUserWorker do
let!(:current_user) { create(:user) }
it "calls the DeleteUserWorker with the params it was given" do
- expect_any_instance_of(DeleteUserService).to receive(:execute).
+ expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, {})
DeleteUserWorker.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
- expect_any_instance_of(DeleteUserService).to receive(:execute).
+ expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, test: "test")
DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test")
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index e471a68a49a..a60af574a08 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -102,12 +102,13 @@ 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],
)
- project.repository.update_ref!(
+ GitOperationService.new(nil, project.repository).send(
+ :update_ref,
"refs/heads/#{SecureRandom.hex(6)}",
new_commit_sha,
Gitlab::Git::BLANK_SHA
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 984acdade36..7bcb5521202 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -74,7 +74,7 @@ describe PostReceive do
context "webhook" do
it "fetches the correct project" do
- expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
+ expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
@@ -89,7 +89,7 @@ describe PostReceive do
end
it "asks the project to trigger all hooks" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
@@ -97,7 +97,7 @@ describe PostReceive do
end
it "enqueues a UpdateMergeRequestsWorker job" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -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_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 59cfb2c8e3a..d2609d21546 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
it 'passes when the project has no push events' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
project.events.destroy_all
break_repo(project)
@@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
project.create_wiki
# Test sanity: everything should be fine before the wiki repo is broken
@@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
FileUtils.rm_rf(wiki_path(project))
subject.perform(project.id)
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/date.format.js b/vendor/assets/javascripts/date.format.js
index f5dc4abcd80..2c9b4825443 100644
--- a/vendor/assets/javascripts/date.format.js
+++ b/vendor/assets/javascripts/date.format.js
@@ -11,115 +11,122 @@
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*/
+ (function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.dateFormat = factory());
+ }(this, (function () { 'use strict';
+ var dateFormat = function () {
+ var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
+ timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
+ timezoneClip = /[^-+\dA-Z]/g,
+ pad = function (val, len) {
+ val = String(val);
+ len = len || 2;
+ while (val.length < len) val = "0" + val;
+ return val;
+ };
-var dateFormat = function () {
- var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
- timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
- timezoneClip = /[^-+\dA-Z]/g,
- pad = function (val, len) {
- val = String(val);
- len = len || 2;
- while (val.length < len) val = "0" + val;
- return val;
- };
+ // Regexes and supporting functions are cached through closure
+ return function (date, mask, utc) {
+ var dF = dateFormat;
- // Regexes and supporting functions are cached through closure
- return function (date, mask, utc) {
- var dF = dateFormat;
+ // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
+ if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
+ mask = date;
+ date = undefined;
+ }
- // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
- mask = date;
- date = undefined;
- }
+ // Passing date through Date applies Date.parse, if necessary
+ date = date ? new Date(date) : new Date;
+ if (isNaN(date)) throw SyntaxError("invalid date");
- // Passing date through Date applies Date.parse, if necessary
- date = date ? new Date(date) : new Date;
- if (isNaN(date)) throw SyntaxError("invalid date");
+ mask = String(dF.masks[mask] || mask || dF.masks["default"]);
- mask = String(dF.masks[mask] || mask || dF.masks["default"]);
+ // Allow setting the utc argument via the mask
+ if (mask.slice(0, 4) == "UTC:") {
+ mask = mask.slice(4);
+ utc = true;
+ }
- // Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
- mask = mask.slice(4);
- utc = true;
- }
+ var _ = utc ? "getUTC" : "get",
+ d = date[_ + "Date"](),
+ D = date[_ + "Day"](),
+ m = date[_ + "Month"](),
+ y = date[_ + "FullYear"](),
+ H = date[_ + "Hours"](),
+ M = date[_ + "Minutes"](),
+ s = date[_ + "Seconds"](),
+ L = date[_ + "Milliseconds"](),
+ o = utc ? 0 : date.getTimezoneOffset(),
+ flags = {
+ d: d,
+ dd: pad(d),
+ ddd: dF.i18n.dayNames[D],
+ dddd: dF.i18n.dayNames[D + 7],
+ m: m + 1,
+ mm: pad(m + 1),
+ mmm: dF.i18n.monthNames[m],
+ mmmm: dF.i18n.monthNames[m + 12],
+ yy: String(y).slice(2),
+ yyyy: y,
+ h: H % 12 || 12,
+ hh: pad(H % 12 || 12),
+ H: H,
+ HH: pad(H),
+ M: M,
+ MM: pad(M),
+ s: s,
+ ss: pad(s),
+ l: pad(L, 3),
+ L: pad(L > 99 ? Math.round(L / 10) : L),
+ t: H < 12 ? "a" : "p",
+ tt: H < 12 ? "am" : "pm",
+ T: H < 12 ? "A" : "P",
+ TT: H < 12 ? "AM" : "PM",
+ Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
+ o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
+ S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
+ };
- var _ = utc ? "getUTC" : "get",
- d = date[_ + "Date"](),
- D = date[_ + "Day"](),
- m = date[_ + "Month"](),
- y = date[_ + "FullYear"](),
- H = date[_ + "Hours"](),
- M = date[_ + "Minutes"](),
- s = date[_ + "Seconds"](),
- L = date[_ + "Milliseconds"](),
- o = utc ? 0 : date.getTimezoneOffset(),
- flags = {
- d: d,
- dd: pad(d),
- ddd: dF.i18n.dayNames[D],
- dddd: dF.i18n.dayNames[D + 7],
- m: m + 1,
- mm: pad(m + 1),
- mmm: dF.i18n.monthNames[m],
- mmmm: dF.i18n.monthNames[m + 12],
- yy: String(y).slice(2),
- yyyy: y,
- h: H % 12 || 12,
- hh: pad(H % 12 || 12),
- H: H,
- HH: pad(H),
- M: M,
- MM: pad(M),
- s: s,
- ss: pad(s),
- l: pad(L, 3),
- L: pad(L > 99 ? Math.round(L / 10) : L),
- t: H < 12 ? "a" : "p",
- tt: H < 12 ? "am" : "pm",
- T: H < 12 ? "A" : "P",
- TT: H < 12 ? "AM" : "PM",
- Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
- o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
- };
+ return mask.replace(token, function ($0) {
+ return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
+ });
+ };
+ }();
- return mask.replace(token, function ($0) {
- return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
- });
+ // Some common format strings
+ dateFormat.masks = {
+ "default": "ddd mmm dd yyyy HH:MM:ss",
+ shortDate: "m/d/yy",
+ mediumDate: "mmm d, yyyy",
+ longDate: "mmmm d, yyyy",
+ fullDate: "dddd, mmmm d, yyyy",
+ shortTime: "h:MM TT",
+ mediumTime: "h:MM:ss TT",
+ longTime: "h:MM:ss TT Z",
+ isoDate: "yyyy-mm-dd",
+ isoTime: "HH:MM:ss",
+ isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
+ isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
-}();
-// Some common format strings
-dateFormat.masks = {
- "default": "ddd mmm dd yyyy HH:MM:ss",
- shortDate: "m/d/yy",
- mediumDate: "mmm d, yyyy",
- longDate: "mmmm d, yyyy",
- fullDate: "dddd, mmmm d, yyyy",
- shortTime: "h:MM TT",
- mediumTime: "h:MM:ss TT",
- longTime: "h:MM:ss TT Z",
- isoDate: "yyyy-mm-dd",
- isoTime: "HH:MM:ss",
- isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
- isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
-};
+ // Internationalization strings
+ dateFormat.i18n = {
+ dayNames: [
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+ ],
+ monthNames: [
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
+ ]
+ };
-// Internationalization strings
-dateFormat.i18n = {
- dayNames: [
- "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
- ],
- monthNames: [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
- "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
- ]
-};
+ // For convenience...
+ Date.prototype.format = function (mask, utc) {
+ return dateFormat(this, mask, utc);
+ };
-// For convenience...
-Date.prototype.format = function (mask, utc) {
- return dateFormat(this, mask, utc);
-};
+ return dateFormat;
+})));
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
deleted file mode 100644
index 19e6c13a655..00000000000
--- a/vendor/assets/javascripts/es6-promise.auto.js
+++ /dev/null
@@ -1,1159 +0,0 @@
-/*!
- * @overview es6-promise - a tiny implementation of Promises/A+.
- * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
- * @license Licensed under MIT license
- * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
- * @version 4.0.5
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.ES6Promise = factory());
-}(this, (function () { 'use strict';
-
-function objectOrFunction(x) {
- return typeof x === 'function' || typeof x === 'object' && x !== null;
-}
-
-function isFunction(x) {
- return typeof x === 'function';
-}
-
-var _isArray = undefined;
-if (!Array.isArray) {
- _isArray = function (x) {
- return Object.prototype.toString.call(x) === '[object Array]';
- };
-} else {
- _isArray = Array.isArray;
-}
-
-var isArray = _isArray;
-
-var len = 0;
-var vertxNext = undefined;
-var customSchedulerFn = undefined;
-
-var asap = function asap(callback, arg) {
- queue[len] = callback;
- queue[len + 1] = arg;
- len += 2;
- if (len === 2) {
- // If len is 2, that means that we need to schedule an async flush.
- // If additional callbacks are queued before the queue is flushed, they
- // will be processed by this flush that we are scheduling.
- if (customSchedulerFn) {
- customSchedulerFn(flush);
- } else {
- scheduleFlush();
- }
- }
-};
-
-function setScheduler(scheduleFn) {
- customSchedulerFn = scheduleFn;
-}
-
-function setAsap(asapFn) {
- asap = asapFn;
-}
-
-var browserWindow = typeof window !== 'undefined' ? window : undefined;
-var browserGlobal = browserWindow || {};
-var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
-var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]';
-
-// test for web worker but not in IE10
-var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
-
-// node
-function useNextTick() {
- // node version 0.10.x displays a deprecation warning when nextTick is used recursively
- // see https://github.com/cujojs/when/issues/410 for details
- return function () {
- return process.nextTick(flush);
- };
-}
-
-// vertx
-function useVertxTimer() {
- if (typeof vertxNext !== 'undefined') {
- return function () {
- vertxNext(flush);
- };
- }
-
- return useSetTimeout();
-}
-
-function useMutationObserver() {
- var iterations = 0;
- var observer = new BrowserMutationObserver(flush);
- var node = document.createTextNode('');
- observer.observe(node, { characterData: true });
-
- return function () {
- node.data = iterations = ++iterations % 2;
- };
-}
-
-// web worker
-function useMessageChannel() {
- var channel = new MessageChannel();
- channel.port1.onmessage = flush;
- return function () {
- return channel.port2.postMessage(0);
- };
-}
-
-function useSetTimeout() {
- // Store setTimeout reference so es6-promise will be unaffected by
- // other code modifying setTimeout (like sinon.useFakeTimers())
- var globalSetTimeout = setTimeout;
- return function () {
- return globalSetTimeout(flush, 1);
- };
-}
-
-var queue = new Array(1000);
-function flush() {
- for (var i = 0; i < len; i += 2) {
- var callback = queue[i];
- var arg = queue[i + 1];
-
- callback(arg);
-
- queue[i] = undefined;
- queue[i + 1] = undefined;
- }
-
- len = 0;
-}
-
-function attemptVertx() {
- try {
- var r = require;
- var vertx = r('vertx');
- vertxNext = vertx.runOnLoop || vertx.runOnContext;
- return useVertxTimer();
- } catch (e) {
- return useSetTimeout();
- }
-}
-
-var scheduleFlush = undefined;
-// Decide what async method to use to triggering processing of queued callbacks:
-if (isNode) {
- scheduleFlush = useNextTick();
-} else if (BrowserMutationObserver) {
- scheduleFlush = useMutationObserver();
-} else if (isWorker) {
- scheduleFlush = useMessageChannel();
-} else if (browserWindow === undefined && typeof require === 'function') {
- scheduleFlush = attemptVertx();
-} else {
- scheduleFlush = useSetTimeout();
-}
-
-function then(onFulfillment, onRejection) {
- var _arguments = arguments;
-
- var parent = this;
-
- var child = new this.constructor(noop);
-
- if (child[PROMISE_ID] === undefined) {
- makePromise(child);
- }
-
- var _state = parent._state;
-
- if (_state) {
- (function () {
- var callback = _arguments[_state - 1];
- asap(function () {
- return invokeCallback(_state, child, callback, parent._result);
- });
- })();
- } else {
- subscribe(parent, child, onFulfillment, onRejection);
- }
-
- return child;
-}
-
-/**
- `Promise.resolve` returns a promise that will become resolved with the
- passed `value`. It is shorthand for the following:
-
- ```javascript
- let promise = new Promise(function(resolve, reject){
- resolve(1);
- });
-
- promise.then(function(value){
- // value === 1
- });
- ```
-
- Instead of writing the above, your code now simply becomes the following:
-
- ```javascript
- let promise = Promise.resolve(1);
-
- promise.then(function(value){
- // value === 1
- });
- ```
-
- @method resolve
- @static
- @param {Any} value value that the returned promise will be resolved with
- Useful for tooling.
- @return {Promise} a promise that will become fulfilled with the given
- `value`
-*/
-function resolve(object) {
- /*jshint validthis:true */
- var Constructor = this;
-
- if (object && typeof object === 'object' && object.constructor === Constructor) {
- return object;
- }
-
- var promise = new Constructor(noop);
- _resolve(promise, object);
- return promise;
-}
-
-var PROMISE_ID = Math.random().toString(36).substring(16);
-
-function noop() {}
-
-var PENDING = void 0;
-var FULFILLED = 1;
-var REJECTED = 2;
-
-var GET_THEN_ERROR = new ErrorObject();
-
-function selfFulfillment() {
- return new TypeError("You cannot resolve a promise with itself");
-}
-
-function cannotReturnOwn() {
- return new TypeError('A promises callback cannot return that same promise.');
-}
-
-function getThen(promise) {
- try {
- return promise.then;
- } catch (error) {
- GET_THEN_ERROR.error = error;
- return GET_THEN_ERROR;
- }
-}
-
-function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
- try {
- then.call(value, fulfillmentHandler, rejectionHandler);
- } catch (e) {
- return e;
- }
-}
-
-function handleForeignThenable(promise, thenable, then) {
- asap(function (promise) {
- var sealed = false;
- var error = tryThen(then, thenable, function (value) {
- if (sealed) {
- return;
- }
- sealed = true;
- if (thenable !== value) {
- _resolve(promise, value);
- } else {
- fulfill(promise, value);
- }
- }, function (reason) {
- if (sealed) {
- return;
- }
- sealed = true;
-
- _reject(promise, reason);
- }, 'Settle: ' + (promise._label || ' unknown promise'));
-
- if (!sealed && error) {
- sealed = true;
- _reject(promise, error);
- }
- }, promise);
-}
-
-function handleOwnThenable(promise, thenable) {
- if (thenable._state === FULFILLED) {
- fulfill(promise, thenable._result);
- } else if (thenable._state === REJECTED) {
- _reject(promise, thenable._result);
- } else {
- subscribe(thenable, undefined, function (value) {
- return _resolve(promise, value);
- }, function (reason) {
- return _reject(promise, reason);
- });
- }
-}
-
-function handleMaybeThenable(promise, maybeThenable, then$$) {
- if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
- handleOwnThenable(promise, maybeThenable);
- } else {
- if (then$$ === GET_THEN_ERROR) {
- _reject(promise, GET_THEN_ERROR.error);
- } else if (then$$ === undefined) {
- fulfill(promise, maybeThenable);
- } else if (isFunction(then$$)) {
- handleForeignThenable(promise, maybeThenable, then$$);
- } else {
- fulfill(promise, maybeThenable);
- }
- }
-}
-
-function _resolve(promise, value) {
- if (promise === value) {
- _reject(promise, selfFulfillment());
- } else if (objectOrFunction(value)) {
- handleMaybeThenable(promise, value, getThen(value));
- } else {
- fulfill(promise, value);
- }
-}
-
-function publishRejection(promise) {
- if (promise._onerror) {
- promise._onerror(promise._result);
- }
-
- publish(promise);
-}
-
-function fulfill(promise, value) {
- if (promise._state !== PENDING) {
- return;
- }
-
- promise._result = value;
- promise._state = FULFILLED;
-
- if (promise._subscribers.length !== 0) {
- asap(publish, promise);
- }
-}
-
-function _reject(promise, reason) {
- if (promise._state !== PENDING) {
- return;
- }
- promise._state = REJECTED;
- promise._result = reason;
-
- asap(publishRejection, promise);
-}
-
-function subscribe(parent, child, onFulfillment, onRejection) {
- var _subscribers = parent._subscribers;
- var length = _subscribers.length;
-
- parent._onerror = null;
-
- _subscribers[length] = child;
- _subscribers[length + FULFILLED] = onFulfillment;
- _subscribers[length + REJECTED] = onRejection;
-
- if (length === 0 && parent._state) {
- asap(publish, parent);
- }
-}
-
-function publish(promise) {
- var subscribers = promise._subscribers;
- var settled = promise._state;
-
- if (subscribers.length === 0) {
- return;
- }
-
- var child = undefined,
- callback = undefined,
- detail = promise._result;
-
- for (var i = 0; i < subscribers.length; i += 3) {
- child = subscribers[i];
- callback = subscribers[i + settled];
-
- if (child) {
- invokeCallback(settled, child, callback, detail);
- } else {
- callback(detail);
- }
- }
-
- promise._subscribers.length = 0;
-}
-
-function ErrorObject() {
- this.error = null;
-}
-
-var TRY_CATCH_ERROR = new ErrorObject();
-
-function tryCatch(callback, detail) {
- try {
- return callback(detail);
- } catch (e) {
- TRY_CATCH_ERROR.error = e;
- return TRY_CATCH_ERROR;
- }
-}
-
-function invokeCallback(settled, promise, callback, detail) {
- var hasCallback = isFunction(callback),
- value = undefined,
- error = undefined,
- succeeded = undefined,
- failed = undefined;
-
- if (hasCallback) {
- value = tryCatch(callback, detail);
-
- if (value === TRY_CATCH_ERROR) {
- failed = true;
- error = value.error;
- value = null;
- } else {
- succeeded = true;
- }
-
- if (promise === value) {
- _reject(promise, cannotReturnOwn());
- return;
- }
- } else {
- value = detail;
- succeeded = true;
- }
-
- if (promise._state !== PENDING) {
- // noop
- } else if (hasCallback && succeeded) {
- _resolve(promise, value);
- } else if (failed) {
- _reject(promise, error);
- } else if (settled === FULFILLED) {
- fulfill(promise, value);
- } else if (settled === REJECTED) {
- _reject(promise, value);
- }
-}
-
-function initializePromise(promise, resolver) {
- try {
- resolver(function resolvePromise(value) {
- _resolve(promise, value);
- }, function rejectPromise(reason) {
- _reject(promise, reason);
- });
- } catch (e) {
- _reject(promise, e);
- }
-}
-
-var id = 0;
-function nextId() {
- return id++;
-}
-
-function makePromise(promise) {
- promise[PROMISE_ID] = id++;
- promise._state = undefined;
- promise._result = undefined;
- promise._subscribers = [];
-}
-
-function Enumerator(Constructor, input) {
- this._instanceConstructor = Constructor;
- this.promise = new Constructor(noop);
-
- if (!this.promise[PROMISE_ID]) {
- makePromise(this.promise);
- }
-
- if (isArray(input)) {
- this._input = input;
- this.length = input.length;
- this._remaining = input.length;
-
- this._result = new Array(this.length);
-
- if (this.length === 0) {
- fulfill(this.promise, this._result);
- } else {
- this.length = this.length || 0;
- this._enumerate();
- if (this._remaining === 0) {
- fulfill(this.promise, this._result);
- }
- }
- } else {
- _reject(this.promise, validationError());
- }
-}
-
-function validationError() {
- return new Error('Array Methods must be provided an Array');
-};
-
-Enumerator.prototype._enumerate = function () {
- var length = this.length;
- var _input = this._input;
-
- for (var i = 0; this._state === PENDING && i < length; i++) {
- this._eachEntry(_input[i], i);
- }
-};
-
-Enumerator.prototype._eachEntry = function (entry, i) {
- var c = this._instanceConstructor;
- var resolve$$ = c.resolve;
-
- if (resolve$$ === resolve) {
- var _then = getThen(entry);
-
- if (_then === then && entry._state !== PENDING) {
- this._settledAt(entry._state, i, entry._result);
- } else if (typeof _then !== 'function') {
- this._remaining--;
- this._result[i] = entry;
- } else if (c === Promise) {
- var promise = new c(noop);
- handleMaybeThenable(promise, entry, _then);
- this._willSettleAt(promise, i);
- } else {
- this._willSettleAt(new c(function (resolve$$) {
- return resolve$$(entry);
- }), i);
- }
- } else {
- this._willSettleAt(resolve$$(entry), i);
- }
-};
-
-Enumerator.prototype._settledAt = function (state, i, value) {
- var promise = this.promise;
-
- if (promise._state === PENDING) {
- this._remaining--;
-
- if (state === REJECTED) {
- _reject(promise, value);
- } else {
- this._result[i] = value;
- }
- }
-
- if (this._remaining === 0) {
- fulfill(promise, this._result);
- }
-};
-
-Enumerator.prototype._willSettleAt = function (promise, i) {
- var enumerator = this;
-
- subscribe(promise, undefined, function (value) {
- return enumerator._settledAt(FULFILLED, i, value);
- }, function (reason) {
- return enumerator._settledAt(REJECTED, i, reason);
- });
-};
-
-/**
- `Promise.all` accepts an array of promises, and returns a new promise which
- is fulfilled with an array of fulfillment values for the passed promises, or
- rejected with the reason of the first passed promise to be rejected. It casts all
- elements of the passed iterable to promises as it runs this algorithm.
-
- Example:
-
- ```javascript
- let promise1 = resolve(1);
- let promise2 = resolve(2);
- let promise3 = resolve(3);
- let promises = [ promise1, promise2, promise3 ];
-
- Promise.all(promises).then(function(array){
- // The array here would be [ 1, 2, 3 ];
- });
- ```
-
- If any of the `promises` given to `all` are rejected, the first promise
- that is rejected will be given as an argument to the returned promises's
- rejection handler. For example:
-
- Example:
-
- ```javascript
- let promise1 = resolve(1);
- let promise2 = reject(new Error("2"));
- let promise3 = reject(new Error("3"));
- let promises = [ promise1, promise2, promise3 ];
-
- Promise.all(promises).then(function(array){
- // Code here never runs because there are rejected promises!
- }, function(error) {
- // error.message === "2"
- });
- ```
-
- @method all
- @static
- @param {Array} entries array of promises
- @param {String} label optional string for labeling the promise.
- Useful for tooling.
- @return {Promise} promise that is fulfilled when all `promises` have been
- fulfilled, or rejected if any of them become rejected.
- @static
-*/
-function all(entries) {
- return new Enumerator(this, entries).promise;
-}
-
-/**
- `Promise.race` returns a new promise which is settled in the same way as the
- first passed promise to settle.
-
- Example:
-
- ```javascript
- let promise1 = new Promise(function(resolve, reject){
- setTimeout(function(){
- resolve('promise 1');
- }, 200);
- });
-
- let promise2 = new Promise(function(resolve, reject){
- setTimeout(function(){
- resolve('promise 2');
- }, 100);
- });
-
- Promise.race([promise1, promise2]).then(function(result){
- // result === 'promise 2' because it was resolved before promise1
- // was resolved.
- });
- ```
-
- `Promise.race` is deterministic in that only the state of the first
- settled promise matters. For example, even if other promises given to the
- `promises` array argument are resolved, but the first settled promise has
- become rejected before the other promises became fulfilled, the returned
- promise will become rejected:
-
- ```javascript
- let promise1 = new Promise(function(resolve, reject){
- setTimeout(function(){
- resolve('promise 1');
- }, 200);
- });
-
- let promise2 = new Promise(function(resolve, reject){
- setTimeout(function(){
- reject(new Error('promise 2'));
- }, 100);
- });
-
- Promise.race([promise1, promise2]).then(function(result){
- // Code here never runs
- }, function(reason){
- // reason.message === 'promise 2' because promise 2 became rejected before
- // promise 1 became fulfilled
- });
- ```
-
- An example real-world use case is implementing timeouts:
-
- ```javascript
- Promise.race([ajax('foo.json'), timeout(5000)])
- ```
-
- @method race
- @static
- @param {Array} promises array of promises to observe
- Useful for tooling.
- @return {Promise} a promise which settles in the same way as the first passed
- promise to settle.
-*/
-function race(entries) {
- /*jshint validthis:true */
- var Constructor = this;
-
- if (!isArray(entries)) {
- return new Constructor(function (_, reject) {
- return reject(new TypeError('You must pass an array to race.'));
- });
- } else {
- return new Constructor(function (resolve, reject) {
- var length = entries.length;
- for (var i = 0; i < length; i++) {
- Constructor.resolve(entries[i]).then(resolve, reject);
- }
- });
- }
-}
-
-/**
- `Promise.reject` returns a promise rejected with the passed `reason`.
- It is shorthand for the following:
-
- ```javascript
- let promise = new Promise(function(resolve, reject){
- reject(new Error('WHOOPS'));
- });
-
- promise.then(function(value){
- // Code here doesn't run because the promise is rejected!
- }, function(reason){
- // reason.message === 'WHOOPS'
- });
- ```
-
- Instead of writing the above, your code now simply becomes the following:
-
- ```javascript
- let promise = Promise.reject(new Error('WHOOPS'));
-
- promise.then(function(value){
- // Code here doesn't run because the promise is rejected!
- }, function(reason){
- // reason.message === 'WHOOPS'
- });
- ```
-
- @method reject
- @static
- @param {Any} reason value that the returned promise will be rejected with.
- Useful for tooling.
- @return {Promise} a promise rejected with the given `reason`.
-*/
-function reject(reason) {
- /*jshint validthis:true */
- var Constructor = this;
- var promise = new Constructor(noop);
- _reject(promise, reason);
- return promise;
-}
-
-function needsResolver() {
- throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
-}
-
-function needsNew() {
- throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
-}
-
-/**
- Promise objects represent the eventual result of an asynchronous operation. The
- primary way of interacting with a promise is through its `then` method, which
- registers callbacks to receive either a promise's eventual value or the reason
- why the promise cannot be fulfilled.
-
- Terminology
- -----------
-
- - `promise` is an object or function with a `then` method whose behavior conforms to this specification.
- - `thenable` is an object or function that defines a `then` method.
- - `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
- - `exception` is a value that is thrown using the throw statement.
- - `reason` is a value that indicates why a promise was rejected.
- - `settled` the final resting state of a promise, fulfilled or rejected.
-
- A promise can be in one of three states: pending, fulfilled, or rejected.
-
- Promises that are fulfilled have a fulfillment value and are in the fulfilled
- state. Promises that are rejected have a rejection reason and are in the
- rejected state. A fulfillment value is never a thenable.
-
- Promises can also be said to *resolve* a value. If this value is also a
- promise, then the original promise's settled state will match the value's
- settled state. So a promise that *resolves* a promise that rejects will
- itself reject, and a promise that *resolves* a promise that fulfills will
- itself fulfill.
-
-
- Basic Usage:
- ------------
-
- ```js
- let promise = new Promise(function(resolve, reject) {
- // on success
- resolve(value);
-
- // on failure
- reject(reason);
- });
-
- promise.then(function(value) {
- // on fulfillment
- }, function(reason) {
- // on rejection
- });
- ```
-
- Advanced Usage:
- ---------------
-
- Promises shine when abstracting away asynchronous interactions such as
- `XMLHttpRequest`s.
-
- ```js
- function getJSON(url) {
- return new Promise(function(resolve, reject){
- let xhr = new XMLHttpRequest();
-
- xhr.open('GET', url);
- xhr.onreadystatechange = handler;
- xhr.responseType = 'json';
- xhr.setRequestHeader('Accept', 'application/json');
- xhr.send();
-
- function handler() {
- if (this.readyState === this.DONE) {
- if (this.status === 200) {
- resolve(this.response);
- } else {
- reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
- }
- }
- };
- });
- }
-
- getJSON('/posts.json').then(function(json) {
- // on fulfillment
- }, function(reason) {
- // on rejection
- });
- ```
-
- Unlike callbacks, promises are great composable primitives.
-
- ```js
- Promise.all([
- getJSON('/posts'),
- getJSON('/comments')
- ]).then(function(values){
- values[0] // => postsJSON
- values[1] // => commentsJSON
-
- return values;
- });
- ```
-
- @class Promise
- @param {function} resolver
- Useful for tooling.
- @constructor
-*/
-function Promise(resolver) {
- this[PROMISE_ID] = nextId();
- this._result = this._state = undefined;
- this._subscribers = [];
-
- if (noop !== resolver) {
- typeof resolver !== 'function' && needsResolver();
- this instanceof Promise ? initializePromise(this, resolver) : needsNew();
- }
-}
-
-Promise.all = all;
-Promise.race = race;
-Promise.resolve = resolve;
-Promise.reject = reject;
-Promise._setScheduler = setScheduler;
-Promise._setAsap = setAsap;
-Promise._asap = asap;
-
-Promise.prototype = {
- constructor: Promise,
-
- /**
- The primary way of interacting with a promise is through its `then` method,
- which registers callbacks to receive either a promise's eventual value or the
- reason why the promise cannot be fulfilled.
-
- ```js
- findUser().then(function(user){
- // user is available
- }, function(reason){
- // user is unavailable, and you are given the reason why
- });
- ```
-
- Chaining
- --------
-
- The return value of `then` is itself a promise. This second, 'downstream'
- promise is resolved with the return value of the first promise's fulfillment
- or rejection handler, or rejected if the handler throws an exception.
-
- ```js
- findUser().then(function (user) {
- return user.name;
- }, function (reason) {
- return 'default name';
- }).then(function (userName) {
- // If `findUser` fulfilled, `userName` will be the user's name, otherwise it
- // will be `'default name'`
- });
-
- findUser().then(function (user) {
- throw new Error('Found user, but still unhappy');
- }, function (reason) {
- throw new Error('`findUser` rejected and we're unhappy');
- }).then(function (value) {
- // never reached
- }, function (reason) {
- // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
- // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
- });
- ```
- If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
-
- ```js
- findUser().then(function (user) {
- throw new PedagogicalException('Upstream error');
- }).then(function (value) {
- // never reached
- }).then(function (value) {
- // never reached
- }, function (reason) {
- // The `PedgagocialException` is propagated all the way down to here
- });
- ```
-
- Assimilation
- ------------
-
- Sometimes the value you want to propagate to a downstream promise can only be
- retrieved asynchronously. This can be achieved by returning a promise in the
- fulfillment or rejection handler. The downstream promise will then be pending
- until the returned promise is settled. This is called *assimilation*.
-
- ```js
- findUser().then(function (user) {
- return findCommentsByAuthor(user);
- }).then(function (comments) {
- // The user's comments are now available
- });
- ```
-
- If the assimliated promise rejects, then the downstream promise will also reject.
-
- ```js
- findUser().then(function (user) {
- return findCommentsByAuthor(user);
- }).then(function (comments) {
- // If `findCommentsByAuthor` fulfills, we'll have the value here
- }, function (reason) {
- // If `findCommentsByAuthor` rejects, we'll have the reason here
- });
- ```
-
- Simple Example
- --------------
-
- Synchronous Example
-
- ```javascript
- let result;
-
- try {
- result = findResult();
- // success
- } catch(reason) {
- // failure
- }
- ```
-
- Errback Example
-
- ```js
- findResult(function(result, err){
- if (err) {
- // failure
- } else {
- // success
- }
- });
- ```
-
- Promise Example;
-
- ```javascript
- findResult().then(function(result){
- // success
- }, function(reason){
- // failure
- });
- ```
-
- Advanced Example
- --------------
-
- Synchronous Example
-
- ```javascript
- let author, books;
-
- try {
- author = findAuthor();
- books = findBooksByAuthor(author);
- // success
- } catch(reason) {
- // failure
- }
- ```
-
- Errback Example
-
- ```js
-
- function foundBooks(books) {
-
- }
-
- function failure(reason) {
-
- }
-
- findAuthor(function(author, err){
- if (err) {
- failure(err);
- // failure
- } else {
- try {
- findBoooksByAuthor(author, function(books, err) {
- if (err) {
- failure(err);
- } else {
- try {
- foundBooks(books);
- } catch(reason) {
- failure(reason);
- }
- }
- });
- } catch(error) {
- failure(err);
- }
- // success
- }
- });
- ```
-
- Promise Example;
-
- ```javascript
- findAuthor().
- then(findBooksByAuthor).
- then(function(books){
- // found books
- }).catch(function(reason){
- // something went wrong
- });
- ```
-
- @method then
- @param {Function} onFulfilled
- @param {Function} onRejected
- Useful for tooling.
- @return {Promise}
- */
- then: then,
-
- /**
- `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
- as the catch block of a try/catch statement.
-
- ```js
- function findAuthor(){
- throw new Error('couldn't find that author');
- }
-
- // synchronous
- try {
- findAuthor();
- } catch(reason) {
- // something went wrong
- }
-
- // async with promises
- findAuthor().catch(function(reason){
- // something went wrong
- });
- ```
-
- @method catch
- @param {Function} onRejection
- Useful for tooling.
- @return {Promise}
- */
- 'catch': function _catch(onRejection) {
- return this.then(null, onRejection);
- }
-};
-
-function polyfill() {
- var local = undefined;
-
- if (typeof global !== 'undefined') {
- local = global;
- } else if (typeof self !== 'undefined') {
- local = self;
- } else {
- try {
- local = Function('return this')();
- } catch (e) {
- throw new Error('polyfill failed because global object is unavailable in this environment');
- }
- }
-
- var P = local.Promise;
-
- if (P) {
- var promiseToString = null;
- try {
- promiseToString = Object.prototype.toString.call(P.resolve());
- } catch (e) {
- // silently ignored
- }
-
- if (promiseToString === '[object Promise]' && !P.cast) {
- return;
- }
- }
-
- local.Promise = Promise;
-}
-
-// Strange compat..
-Promise.polyfill = polyfill;
-Promise.Promise = Promise;
-
-return Promise;
-
-})));
-
-ES6Promise.polyfill();
-//# sourceMappingURL=es6-promise.auto.map
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.atwho.js b/vendor/assets/javascripts/jquery.atwho.js
new file mode 100644
index 00000000000..0d295ebe5af
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.atwho.js
@@ -0,0 +1,1202 @@
+/**
+ * at.js - 1.5.1
+ * Copyright (c) 2016 chord.luo <chord.luo@gmail.com>;
+ * Homepage: http://ichord.github.com/At.js
+ * License: MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+var DEFAULT_CALLBACKS, KEY_CODE;
+
+KEY_CODE = {
+ DOWN: 40,
+ UP: 38,
+ ESC: 27,
+ TAB: 9,
+ ENTER: 13,
+ CTRL: 17,
+ A: 65,
+ P: 80,
+ N: 78,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ BACKSPACE: 8,
+ SPACE: 32
+};
+
+DEFAULT_CALLBACKS = {
+ beforeSave: function(data) {
+ return Controller.arrayToDefaultHash(data);
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var _a, _y, match, regexp, space;
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ if (should_startWithSpace) {
+ flag = '(?:^|\\s)' + flag;
+ }
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+ space = acceptSpaceBar ? "\ " : "";
+ regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
+ match = regexp.exec(subtext);
+ if (match) {
+ return match[2] || match[1];
+ } else {
+ return null;
+ }
+ },
+ filter: function(query, data, searchKey) {
+ var _results, i, item, len;
+ _results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
+ _results.push(item);
+ }
+ }
+ return _results;
+ },
+ remoteFilter: null,
+ sorter: function(query, items, searchKey) {
+ var _results, i, item, len;
+ if (!query) {
+ return items;
+ }
+ _results = [];
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
+ if (item.atwho_order > -1) {
+ _results.push(item);
+ }
+ }
+ return _results.sort(function(a, b) {
+ return a.atwho_order - b.atwho_order;
+ });
+ },
+ tplEval: function(tpl, map) {
+ var error, error1, template;
+ template = tpl;
+ try {
+ if (typeof tpl !== 'string') {
+ template = tpl(map);
+ }
+ return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+ return map[key];
+ });
+ } catch (error1) {
+ error = error1;
+ return "";
+ }
+ },
+ highlighter: function(li, query) {
+ var regexp;
+ if (!query) {
+ return li;
+ }
+ regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
+ return li.replace(regexp, function(str, $1, $2, $3) {
+ return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+ });
+ },
+ beforeInsert: function(value, $li, e) {
+ return value;
+ },
+ beforeReposition: function(offset) {
+ return offset;
+ },
+ afterMatchFailed: function(at, el) {}
+};
+
+var App;
+
+App = (function() {
+ function App(inputor) {
+ this.currentFlag = null;
+ this.controllers = {};
+ this.aliasMaps = {};
+ this.$inputor = $(inputor);
+ this.setupRootElement();
+ this.listen();
+ }
+
+ App.prototype.createContainer = function(doc) {
+ var ref;
+ if ((ref = this.$el) != null) {
+ ref.remove();
+ }
+ return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>"));
+ };
+
+ App.prototype.setupRootElement = function(iframe, asRoot) {
+ var error, error1;
+ if (asRoot == null) {
+ asRoot = false;
+ }
+ if (iframe) {
+ this.window = iframe.contentWindow;
+ this.document = iframe.contentDocument || this.window.document;
+ this.iframe = iframe;
+ } else {
+ this.document = this.$inputor[0].ownerDocument;
+ this.window = this.document.defaultView || this.document.parentWindow;
+ try {
+ this.iframe = this.window.frameElement;
+ } catch (error1) {
+ error = error1;
+ this.iframe = null;
+ if ($.fn.atwho.debug) {
+ throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
+ }
+ }
+ }
+ return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
+ };
+
+ App.prototype.controller = function(at) {
+ var c, current, currentFlag, ref;
+ if (this.aliasMaps[at]) {
+ current = this.controllers[this.aliasMaps[at]];
+ } else {
+ ref = this.controllers;
+ for (currentFlag in ref) {
+ c = ref[currentFlag];
+ if (currentFlag === at) {
+ current = c;
+ break;
+ }
+ }
+ }
+ if (current) {
+ return current;
+ } else {
+ return this.controllers[this.currentFlag];
+ }
+ };
+
+ App.prototype.setContextFor = function(at) {
+ this.currentFlag = at;
+ return this;
+ };
+
+ App.prototype.reg = function(flag, setting) {
+ var base, controller;
+ controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
+ if (setting.alias) {
+ this.aliasMaps[setting.alias] = flag;
+ }
+ controller.init(setting);
+ return this;
+ };
+
+ App.prototype.listen = function() {
+ return this.$inputor.on('compositionstart', (function(_this) {
+ return function(e) {
+ var ref;
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide();
+ }
+ _this.isComposing = true;
+ return null;
+ };
+ })(this)).on('compositionend', (function(_this) {
+ return function(e) {
+ _this.isComposing = false;
+ setTimeout(function(e) {
+ return _this.dispatch(e);
+ });
+ return null;
+ };
+ })(this)).on('keyup.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeyup(e);
+ };
+ })(this)).on('keydown.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeydown(e);
+ };
+ })(this)).on('blur.atwhoInner', (function(_this) {
+ return function(e) {
+ var c;
+ if (c = _this.controller()) {
+ c.expectedQueryCBId = null;
+ return c.view.hide(e, c.getOpt("displayTimeout"));
+ }
+ };
+ })(this)).on('click.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.dispatch(e);
+ };
+ })(this)).on('scroll.atwhoInner', (function(_this) {
+ return function() {
+ var lastScrollTop;
+ lastScrollTop = _this.$inputor.scrollTop();
+ return function(e) {
+ var currentScrollTop, ref;
+ currentScrollTop = e.target.scrollTop;
+ if (lastScrollTop !== currentScrollTop) {
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide(e);
+ }
+ }
+ lastScrollTop = currentScrollTop;
+ return true;
+ };
+ };
+ })(this)());
+ };
+
+ App.prototype.shutdown = function() {
+ var _, c, ref;
+ ref = this.controllers;
+ for (_ in ref) {
+ c = ref[_];
+ c.destroy();
+ delete this.controllers[_];
+ }
+ this.$inputor.off('.atwhoInner');
+ return this.$el.remove();
+ };
+
+ App.prototype.dispatch = function(e) {
+ var _, c, ref, results;
+ ref = this.controllers;
+ results = [];
+ for (_ in ref) {
+ c = ref[_];
+ results.push(c.lookUp(e));
+ }
+ return results;
+ };
+
+ App.prototype.onKeyup = function(e) {
+ var ref;
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ if ((ref = this.controller()) != null) {
+ ref.view.hide();
+ }
+ break;
+ case KEY_CODE.DOWN:
+ case KEY_CODE.UP:
+ case KEY_CODE.CTRL:
+ case KEY_CODE.ENTER:
+ $.noop();
+ break;
+ case KEY_CODE.P:
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ this.dispatch(e);
+ }
+ break;
+ default:
+ this.dispatch(e);
+ }
+ };
+
+ App.prototype.onKeydown = function(e) {
+ var ref, view;
+ view = (ref = this.controller()) != null ? ref.view : void 0;
+ if (!(view && view.visible())) {
+ return;
+ }
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ view.hide(e);
+ break;
+ case KEY_CODE.UP:
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.DOWN:
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.P:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.TAB:
+ case KEY_CODE.ENTER:
+ case KEY_CODE.SPACE:
+ if (!view.visible()) {
+ return;
+ }
+ if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
+ return;
+ }
+ if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
+ return;
+ }
+ if (view.highlighted()) {
+ e.preventDefault();
+ view.choose(e);
+ } else {
+ view.hide(e);
+ }
+ break;
+ default:
+ $.noop();
+ }
+ };
+
+ return App;
+
+})();
+
+var Controller,
+ slice = [].slice;
+
+Controller = (function() {
+ Controller.prototype.uid = function() {
+ return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+ };
+
+ function Controller(app, at1) {
+ this.app = app;
+ this.at = at1;
+ this.$inputor = this.app.$inputor;
+ this.id = this.$inputor[0].id || this.uid();
+ this.expectedQueryCBId = null;
+ this.setting = null;
+ this.query = null;
+ this.pos = 0;
+ this.range = null;
+ if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
+ this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
+ }
+ this.model = new Model(this);
+ this.view = new View(this);
+ }
+
+ Controller.prototype.init = function(setting) {
+ this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
+ this.view.init();
+ return this.model.reload(this.setting.data);
+ };
+
+ Controller.prototype.destroy = function() {
+ this.trigger('beforeDestroy');
+ this.model.destroy();
+ this.view.destroy();
+ return this.$el.remove();
+ };
+
+ Controller.prototype.callDefault = function() {
+ var args, error, error1, funcName;
+ funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+ try {
+ return DEFAULT_CALLBACKS[funcName].apply(this, args);
+ } catch (error1) {
+ error = error1;
+ return $.error(error + " Or maybe At.js doesn't have function " + funcName);
+ }
+ };
+
+ Controller.prototype.trigger = function(name, data) {
+ var alias, eventName;
+ if (data == null) {
+ data = [];
+ }
+ data.push(this);
+ alias = this.getOpt('alias');
+ eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
+ return this.$inputor.trigger(eventName, data);
+ };
+
+ Controller.prototype.callbacks = function(funcName) {
+ return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
+ };
+
+ Controller.prototype.getOpt = function(at, default_value) {
+ var e, error1;
+ try {
+ return this.setting[at];
+ } catch (error1) {
+ e = error1;
+ return null;
+ }
+ };
+
+ Controller.prototype.insertContentFor = function($li) {
+ var data, tpl;
+ tpl = this.getOpt('insertTpl');
+ data = $.extend({}, $li.data('item-data'), {
+ 'atwho-at': this.at
+ });
+ return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
+ };
+
+ Controller.prototype.renderView = function(data) {
+ var searchKey;
+ searchKey = this.getOpt("searchKey");
+ data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
+ return this.view.render(data.slice(0, this.getOpt('limit')));
+ };
+
+ Controller.arrayToDefaultHash = function(data) {
+ var i, item, len, results;
+ if (!$.isArray(data)) {
+ return data;
+ }
+ results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if ($.isPlainObject(item)) {
+ results.push(item);
+ } else {
+ results.push({
+ name: item
+ });
+ }
+ }
+ return results;
+ };
+
+ Controller.prototype.lookUp = function(e) {
+ var query, wait;
+ if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
+ return;
+ }
+ if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
+ return;
+ }
+ query = this.catchQuery(e);
+ if (!query) {
+ this.expectedQueryCBId = null;
+ return query;
+ }
+ this.app.setContextFor(this.at);
+ if (wait = this.getOpt('delay')) {
+ this._delayLookUp(query, wait);
+ } else {
+ this._lookUp(query);
+ }
+ return query;
+ };
+
+ Controller.prototype._delayLookUp = function(query, wait) {
+ var now, remaining;
+ now = Date.now ? Date.now() : new Date().getTime();
+ this.previousCallTime || (this.previousCallTime = now);
+ remaining = wait - (now - this.previousCallTime);
+ if ((0 < remaining && remaining < wait)) {
+ this.previousCallTime = now;
+ this._stopDelayedCall();
+ return this.delayedCallTimeout = setTimeout((function(_this) {
+ return function() {
+ _this.previousCallTime = 0;
+ _this.delayedCallTimeout = null;
+ return _this._lookUp(query);
+ };
+ })(this), wait);
+ } else {
+ this._stopDelayedCall();
+ if (this.previousCallTime !== now) {
+ this.previousCallTime = 0;
+ }
+ return this._lookUp(query);
+ }
+ };
+
+ Controller.prototype._stopDelayedCall = function() {
+ if (this.delayedCallTimeout) {
+ clearTimeout(this.delayedCallTimeout);
+ return this.delayedCallTimeout = null;
+ }
+ };
+
+ Controller.prototype._generateQueryCBId = function() {
+ return {};
+ };
+
+ Controller.prototype._lookUp = function(query) {
+ var _callback;
+ _callback = function(queryCBId, data) {
+ if (queryCBId !== this.expectedQueryCBId) {
+ return;
+ }
+ if (data && data.length > 0) {
+ return this.renderView(this.constructor.arrayToDefaultHash(data));
+ } else {
+ return this.view.hide();
+ }
+ };
+ this.expectedQueryCBId = this._generateQueryCBId();
+ return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
+ };
+
+ return Controller;
+
+})();
+
+var TextareaController,
+ 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;
+
+TextareaController = (function(superClass) {
+ extend(TextareaController, superClass);
+
+ function TextareaController() {
+ return TextareaController.__super__.constructor.apply(this, arguments);
+ }
+
+ TextareaController.prototype.catchQuery = function() {
+ var caretPos, content, end, isString, query, start, subtext;
+ content = this.$inputor.val();
+ caretPos = this.$inputor.caret('pos', {
+ iframe: this.app.iframe
+ });
+ subtext = content.slice(0, caretPos);
+ query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof query === 'string';
+ if (isString && query.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && query.length <= this.getOpt('maxLen', 20)) {
+ start = caretPos - query.length;
+ end = start + query.length;
+ this.pos = start;
+ query = {
+ 'text': query,
+ 'headPos': start,
+ 'endPos': end
+ };
+ this.trigger("matched", [this.at, query.text]);
+ } else {
+ query = null;
+ this.view.hide();
+ }
+ return this.query = query;
+ };
+
+ TextareaController.prototype.rect = function() {
+ var c, iframeOffset, scaleBottom;
+ if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+ iframe: this.app.iframe
+ }))) {
+ return;
+ }
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = $(this.app.iframe).offset();
+ c.left += iframeOffset.left;
+ c.top += iframeOffset.top;
+ }
+ scaleBottom = this.app.document.selection ? 0 : 2;
+ return {
+ left: c.left,
+ top: c.top,
+ bottom: c.top + c.height + scaleBottom
+ };
+ };
+
+ TextareaController.prototype.insert = function(content, $li) {
+ var $inputor, source, startStr, suffix, text;
+ $inputor = this.$inputor;
+ source = $inputor.val();
+ startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
+ content += suffix;
+ text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
+ $inputor.val(text);
+ $inputor.caret('pos', startStr.length + content.length, {
+ iframe: this.app.iframe
+ });
+ if (!$inputor.is(':focus')) {
+ $inputor.focus();
+ }
+ return $inputor.change();
+ };
+
+ return TextareaController;
+
+})(Controller);
+
+var EditableController,
+ 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;
+
+EditableController = (function(superClass) {
+ extend(EditableController, superClass);
+
+ function EditableController() {
+ return EditableController.__super__.constructor.apply(this, arguments);
+ }
+
+ EditableController.prototype._getRange = function() {
+ var sel;
+ sel = this.app.window.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ }
+ };
+
+ EditableController.prototype._setRange = function(position, node, range) {
+ if (range == null) {
+ range = this._getRange();
+ }
+ if (!range) {
+ return;
+ }
+ node = $(node)[0];
+ if (position === 'after') {
+ range.setEndAfter(node);
+ range.setStartAfter(node);
+ } else {
+ range.setEndBefore(node);
+ range.setStartBefore(node);
+ }
+ range.collapse(false);
+ return this._clearRange(range);
+ };
+
+ EditableController.prototype._clearRange = function(range) {
+ var sel;
+ if (range == null) {
+ range = this._getRange();
+ }
+ sel = this.app.window.getSelection();
+ if (this.ctrl_a_pressed == null) {
+ sel.removeAllRanges();
+ return sel.addRange(range);
+ }
+ };
+
+ EditableController.prototype._movingEvent = function(e) {
+ var ref;
+ return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
+ };
+
+ EditableController.prototype._unwrap = function(node) {
+ var next;
+ node = $(node).unwrap().get(0);
+ if ((next = node.nextSibling) && next.nodeValue) {
+ node.nodeValue += next.nodeValue;
+ $(next).remove();
+ }
+ return node;
+ };
+
+ EditableController.prototype.catchQuery = function(e) {
+ var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
+ if (!(range = this._getRange())) {
+ return;
+ }
+ if (!range.collapsed) {
+ return;
+ }
+ if (e.which === KEY_CODE.ENTER) {
+ ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
+ if ($query.is(':empty')) {
+ $query.remove();
+ }
+ ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
+ this._clearRange();
+ return;
+ }
+ if (/firefox/i.test(navigator.userAgent)) {
+ if ($(range.startContainer).is(this.$inputor)) {
+ this._clearRange();
+ return;
+ }
+ if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, offset);
+ if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
+ inserted = $(range.startContainer).contents().get(offset);
+ this._setRange('after', $(inserted).contents().last());
+ }
+ } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
+ $inserted = $(range.startContainer.previousSibling);
+ if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
+ this._setRange('after', $inserted.contents().last());
+ }
+ }
+ }
+ $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
+ if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
+ $query.remove();
+ }
+ if (!this._movingEvent(e)) {
+ $query.removeClass('atwho-inserted');
+ }
+ if ($query.length > 0) {
+ switch (e.which) {
+ case KEY_CODE.LEFT:
+ this._setRange('before', $query.get(0), range);
+ $query.removeClass('atwho-query');
+ return;
+ case KEY_CODE.RIGHT:
+ this._setRange('after', $query.get(0).nextSibling, range);
+ $query.removeClass('atwho-query');
+ return;
+ }
+ }
+ if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
+ $query.empty().html(query_content).attr('data-atwho-at-query', null);
+ this._setRange('after', $query.get(0), range);
+ }
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, 0);
+ matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof matched === 'string';
+ if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
+ range.setStart(range.startContainer, index);
+ $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
+ range.surroundContents($query.get(0));
+ lastNode = $query.contents().last().get(0);
+ if (/firefox/i.test(navigator.userAgent)) {
+ range.setStart(lastNode, lastNode.length);
+ range.setEnd(lastNode, lastNode.length);
+ this._clearRange(range);
+ } else {
+ this._setRange('after', lastNode, range);
+ }
+ }
+ if (isString && matched.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && matched.length <= this.getOpt('maxLen', 20)) {
+ query = {
+ text: matched,
+ el: $query
+ };
+ this.trigger("matched", [this.at, query.text]);
+ return this.query = query;
+ } else {
+ this.view.hide();
+ this.query = {
+ el: $query
+ };
+ if ($query.text().indexOf(this.at) >= 0) {
+ if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
+ $query.removeClass('atwho-query');
+ } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
+ this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
+ }
+ }
+ return null;
+ }
+ };
+
+ EditableController.prototype.rect = function() {
+ var $iframe, iframeOffset, rect;
+ rect = this.query.el.offset();
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = ($iframe = $(this.app.iframe)).offset();
+ rect.left += iframeOffset.left - this.$inputor.scrollLeft();
+ rect.top += iframeOffset.top - this.$inputor.scrollTop();
+ }
+ rect.bottom = rect.top + this.query.el.height();
+ return rect;
+ };
+
+ EditableController.prototype.insert = function(content, $li) {
+ var data, range, suffix, suffixNode;
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
+ data = $li.data('item-data');
+ this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text);
+ if (range = this._getRange()) {
+ range.setEndAfter(this.query.el[0]);
+ range.collapse(false);
+ range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix));
+ this._setRange('after', suffixNode, range);
+ }
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ return this.$inputor.change();
+ };
+
+ return EditableController;
+
+})(Controller);
+
+var Model;
+
+Model = (function() {
+ function Model(context) {
+ this.context = context;
+ this.at = this.context.at;
+ this.storage = this.context.$inputor;
+ }
+
+ Model.prototype.destroy = function() {
+ return this.storage.data(this.at, null);
+ };
+
+ Model.prototype.saved = function() {
+ return this.fetch() > 0;
+ };
+
+ Model.prototype.query = function(query, callback) {
+ var _remoteFilter, data, searchKey;
+ data = this.fetch();
+ searchKey = this.context.getOpt("searchKey");
+ data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
+ _remoteFilter = this.context.callbacks('remoteFilter');
+ if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
+ return callback(data);
+ } else {
+ return _remoteFilter.call(this.context, query, callback);
+ }
+ };
+
+ Model.prototype.fetch = function() {
+ return this.storage.data(this.at) || [];
+ };
+
+ Model.prototype.save = function(data) {
+ return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
+ };
+
+ Model.prototype.load = function(data) {
+ if (!(this.saved() || !data)) {
+ return this._load(data);
+ }
+ };
+
+ Model.prototype.reload = function(data) {
+ return this._load(data);
+ };
+
+ Model.prototype._load = function(data) {
+ if (typeof data === "string") {
+ return $.ajax(data, {
+ dataType: "json"
+ }).done((function(_this) {
+ return function(data) {
+ return _this.save(data);
+ };
+ })(this));
+ } else {
+ return this.save(data);
+ }
+ };
+
+ return Model;
+
+})();
+
+var View;
+
+View = (function() {
+ function View(context) {
+ this.context = context;
+ this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
+ this.$elUl = this.$el.children();
+ this.timeoutID = null;
+ this.context.$el.append(this.$el);
+ this.bindEvent();
+ }
+
+ View.prototype.init = function() {
+ var header_tpl, id;
+ id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
+ header_tpl = this.context.getOpt("headerTpl");
+ if (header_tpl && this.$el.children().length === 1) {
+ this.$el.prepend(header_tpl);
+ }
+ return this.$el.attr({
+ 'id': "at-view-" + id
+ });
+ };
+
+ View.prototype.destroy = function() {
+ return this.$el.remove();
+ };
+
+ View.prototype.bindEvent = function() {
+ var $menu, lastCoordX, lastCoordY;
+ $menu = this.$el.find('ul');
+ lastCoordX = 0;
+ lastCoordY = 0;
+ return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ var $cur;
+ if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
+ return;
+ }
+ lastCoordX = e.clientX;
+ lastCoordY = e.clientY;
+ $cur = $(e.currentTarget);
+ if ($cur.hasClass('cur')) {
+ return;
+ }
+ $menu.find('.cur').removeClass('cur');
+ return $cur.addClass('cur');
+ };
+ })(this)).on('click.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ $menu.find('.cur').removeClass('cur');
+ $(e.currentTarget).addClass('cur');
+ _this.choose(e);
+ return e.preventDefault();
+ };
+ })(this));
+ };
+
+ View.prototype.visible = function() {
+ return this.$el.is(":visible");
+ };
+
+ View.prototype.highlighted = function() {
+ return this.$el.find(".cur").length > 0;
+ };
+
+ View.prototype.choose = function(e) {
+ var $li, content;
+ if (($li = this.$el.find(".cur")).length) {
+ content = this.context.insertContentFor($li);
+ this.context._stopDelayedCall();
+ this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
+ this.context.trigger("inserted", [$li, e]);
+ this.hide(e);
+ }
+ if (this.context.getOpt("hideWithoutSuffix")) {
+ return this.stopShowing = true;
+ }
+ };
+
+ View.prototype.reposition = function(rect) {
+ var _window, offset, overflowOffset, ref;
+ _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
+ if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
+ rect.bottom = rect.top - this.$el.height();
+ }
+ if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
+ rect.left = overflowOffset;
+ }
+ offset = {
+ left: rect.left,
+ top: rect.bottom
+ };
+ if ((ref = this.context.callbacks("beforeReposition")) != null) {
+ ref.call(this.context, offset);
+ }
+ this.$el.offset(offset);
+ return this.context.trigger("reposition", [offset]);
+ };
+
+ View.prototype.next = function() {
+ var cur, next, nextEl, offset;
+ cur = this.$el.find('.cur').removeClass('cur');
+ next = cur.next();
+ if (!next.length) {
+ next = this.$el.find('li:first');
+ }
+ next.addClass('cur');
+ nextEl = next[0];
+ offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.prev = function() {
+ var cur, offset, prev, prevEl;
+ cur = this.$el.find('.cur').removeClass('cur');
+ prev = cur.prev();
+ if (!prev.length) {
+ prev = this.$el.find('li:last');
+ }
+ prev.addClass('cur');
+ prevEl = prev[0];
+ offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.scrollTop = function(scrollTop) {
+ var scrollDuration;
+ scrollDuration = this.context.getOpt('scrollDuration');
+ if (scrollDuration) {
+ return this.$elUl.animate({
+ scrollTop: scrollTop
+ }, scrollDuration);
+ } else {
+ return this.$elUl.scrollTop(scrollTop);
+ }
+ };
+
+ View.prototype.show = function() {
+ var rect;
+ if (this.stopShowing) {
+ this.stopShowing = false;
+ return;
+ }
+ if (!this.visible()) {
+ this.$el.show();
+ this.$el.scrollTop(0);
+ this.context.trigger('shown');
+ }
+ if (rect = this.context.rect()) {
+ return this.reposition(rect);
+ }
+ };
+
+ View.prototype.hide = function(e, time) {
+ var callback;
+ if (!this.visible()) {
+ return;
+ }
+ if (isNaN(time)) {
+ this.$el.hide();
+ return this.context.trigger('hidden', [e]);
+ } else {
+ callback = (function(_this) {
+ return function() {
+ return _this.hide();
+ };
+ })(this);
+ clearTimeout(this.timeoutID);
+ return this.timeoutID = setTimeout(callback, time);
+ }
+ };
+
+ View.prototype.render = function(list) {
+ var $li, $ul, i, item, len, li, tpl;
+ if (!($.isArray(list) && list.length > 0)) {
+ this.hide();
+ return;
+ }
+ this.$el.find('ul').empty();
+ $ul = this.$el.find('ul');
+ tpl = this.context.getOpt('displayTpl');
+ for (i = 0, len = list.length; i < len; i++) {
+ item = list[i];
+ item = $.extend({}, item, {
+ 'atwho-at': this.context.at
+ });
+ li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
+ $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
+ $li.data("item-data", item);
+ $ul.append($li);
+ }
+ this.show();
+ if (this.context.getOpt('highlightFirst')) {
+ return $ul.find("li:first").addClass("cur");
+ }
+ };
+
+ return View;
+
+})();
+
+var Api;
+
+Api = {
+ load: function(at, data) {
+ var c;
+ if (c = this.controller(at)) {
+ return c.model.load(data);
+ }
+ },
+ isSelecting: function() {
+ var ref;
+ return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
+ },
+ hide: function() {
+ var ref;
+ return (ref = this.controller()) != null ? ref.view.hide() : void 0;
+ },
+ reposition: function() {
+ var c;
+ if (c = this.controller()) {
+ return c.view.reposition(c.rect());
+ }
+ },
+ setIframe: function(iframe, asRoot) {
+ this.setupRootElement(iframe, asRoot);
+ return null;
+ },
+ run: function() {
+ return this.dispatch();
+ },
+ destroy: function() {
+ this.shutdown();
+ return this.$inputor.data('atwho', null);
+ }
+};
+
+$.fn.atwho = function(method) {
+ var _args, result;
+ _args = arguments;
+ result = null;
+ this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
+ var $this, app;
+ if (!(app = ($this = $(this)).data("atwho"))) {
+ $this.data('atwho', (app = new App(this)));
+ }
+ if (typeof method === 'object' || !method) {
+ return app.reg(method.at, method);
+ } else if (Api[method] && app) {
+ return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.atwho");
+ }
+ });
+ if (result != null) {
+ return result;
+ } else {
+ return this;
+ }
+};
+
+$.fn.atwho["default"] = {
+ at: void 0,
+ alias: void 0,
+ data: null,
+ displayTpl: "<li>${name}</li>",
+ insertTpl: "${atwho-at}${name}",
+ headerTpl: null,
+ callbacks: DEFAULT_CALLBACKS,
+ searchKey: "name",
+ suffix: void 0,
+ hideWithoutSuffix: false,
+ startWithSpace: true,
+ acceptSpaceBar: false,
+ highlightFirst: true,
+ limit: 5,
+ maxLen: 20,
+ minLen: 0,
+ displayTimeout: 300,
+ delay: null,
+ spaceSelectsMatch: false,
+ tabSelectsMatch: true,
+ editableAtwhoQueryAttrs: {},
+ scrollDuration: 150,
+ suspendOnComposing: true,
+ lookUpOnClick: true
+};
+
+$.fn.atwho.debug = false;
+
+}));
diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js
new file mode 100644
index 00000000000..811ec63ee47
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.caret.js
@@ -0,0 +1,436 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(["jquery"], function ($) {
+ return (root.returnExportsGlobal = factory($));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+ function EditableCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ EditableCaret.prototype.setPos = function(pos) {
+ var fn, found, offset, sel;
+ if (sel = oWindow.getSelection()) {
+ offset = 0;
+ found = false;
+ (fn = function(pos, parent) {
+ var node, range, _i, _len, _ref, _results;
+ _ref = parent.childNodes;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ node = _ref[_i];
+ if (found) {
+ break;
+ }
+ if (node.nodeType === 3) {
+ if (offset + node.length >= pos) {
+ found = true;
+ range = oDocument.createRange();
+ range.setStart(node, pos - offset);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ break;
+ } else {
+ _results.push(offset += node.length);
+ }
+ } else {
+ _results.push(fn(pos, node));
+ }
+ }
+ return _results;
+ })(pos, this.domInputor);
+ }
+ return this.domInputor;
+ };
+
+ EditableCaret.prototype.getIEPosition = function() {
+ return this.getPosition();
+ };
+
+ EditableCaret.prototype.getPosition = function() {
+ var inputor_offset, offset;
+ offset = this.getOffset();
+ inputor_offset = this.$inputor.offset();
+ offset.left -= inputor_offset.left;
+ offset.top -= inputor_offset.top;
+ return offset;
+ };
+
+ EditableCaret.prototype.getOldIEPos = function() {
+ var preCaretTextRange, textRange;
+ textRange = oDocument.selection.createRange();
+ preCaretTextRange = oDocument.body.createTextRange();
+ preCaretTextRange.moveToElementText(this.domInputor);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ return preCaretTextRange.text.length;
+ };
+
+ EditableCaret.prototype.getPos = function() {
+ var clonedRange, pos, range;
+ if (range = this.range()) {
+ clonedRange = range.cloneRange();
+ clonedRange.selectNodeContents(this.domInputor);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ pos = clonedRange.toString().length;
+ clonedRange.detach();
+ return pos;
+ } else if (oDocument.selection) {
+ return this.getOldIEPos();
+ }
+ };
+
+ EditableCaret.prototype.getOldIEOffset = function() {
+ var range, rect;
+ range = oDocument.selection.createRange().duplicate();
+ range.moveStart("character", -1);
+ rect = range.getBoundingClientRect();
+ return {
+ height: rect.bottom - rect.top,
+ left: rect.left,
+ top: rect.top
+ };
+ };
+
+ EditableCaret.prototype.getOffset = function(pos) {
+ var clonedRange, offset, range, rect, shadowCaret;
+ if (oWindow.getSelection && (range = this.range())) {
+ if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) {
+ clonedRange = range.cloneRange();
+ clonedRange.setStart(range.endContainer, range.endOffset - 1);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left + rect.width,
+ top: rect.top
+ };
+ clonedRange.detach();
+ }
+ if (!offset || (offset != null ? offset.height : void 0) === 0) {
+ clonedRange = range.cloneRange();
+ shadowCaret = $(oDocument.createTextNode("|"));
+ clonedRange.insertNode(shadowCaret[0]);
+ clonedRange.selectNode(shadowCaret[0]);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left,
+ top: rect.top
+ };
+ shadowCaret.remove();
+ clonedRange.detach();
+ }
+ } else if (oDocument.selection) {
+ offset = this.getOldIEOffset();
+ }
+ if (offset) {
+ offset.top += $(oWindow).scrollTop();
+ offset.left += $(oWindow).scrollLeft();
+ }
+ return offset;
+ };
+
+ EditableCaret.prototype.range = function() {
+ var sel;
+ if (!oWindow.getSelection) {
+ return;
+ }
+ sel = oWindow.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ } else {
+ return null;
+ }
+ };
+
+ return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+ function InputCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ InputCaret.prototype.getIEPos = function() {
+ var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+ inputor = this.domInputor;
+ range = oDocument.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ len = normalizedValue.length;
+ textInputRange = inputor.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ endRange.collapse(false);
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ pos = len;
+ } else {
+ pos = -textInputRange.moveStart("character", -len);
+ }
+ }
+ return pos;
+ };
+
+ InputCaret.prototype.getPos = function() {
+ if (oDocument.selection) {
+ return this.getIEPos();
+ } else {
+ return this.domInputor.selectionStart;
+ }
+ };
+
+ InputCaret.prototype.setPos = function(pos) {
+ var inputor, range;
+ inputor = this.domInputor;
+ if (oDocument.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ range.select();
+ } else if (inputor.setSelectionRange) {
+ inputor.setSelectionRange(pos, pos);
+ }
+ return inputor;
+ };
+
+ InputCaret.prototype.getIEOffset = function(pos) {
+ var h, textRange, x, y;
+ textRange = this.domInputor.createTextRange();
+ pos || (pos = this.getPos());
+ textRange.move('character', pos);
+ x = textRange.boundingLeft;
+ y = textRange.boundingTop;
+ h = textRange.boundingHeight;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ InputCaret.prototype.getOffset = function(pos) {
+ var $inputor, offset, position;
+ $inputor = this.$inputor;
+ if (oDocument.selection) {
+ offset = this.getIEOffset(pos);
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+ return offset;
+ } else {
+ offset = $inputor.offset();
+ position = this.getPosition(pos);
+ return offset = {
+ left: offset.left + position.left - $inputor.scrollLeft(),
+ top: offset.top + position.top - $inputor.scrollTop(),
+ height: position.height
+ };
+ }
+ };
+
+ InputCaret.prototype.getPosition = function(pos) {
+ var $inputor, at_rect, end_range, format, html, mirror, start_range;
+ $inputor = this.$inputor;
+ format = function(value) {
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>");
+ if (/firefox/i.test(navigator.userAgent)) {
+ value = value.replace(/\s/g, '&nbsp;');
+ }
+ return value;
+ };
+ if (pos === void 0) {
+ pos = this.getPos();
+ }
+ start_range = $inputor.val().slice(0, pos);
+ end_range = $inputor.val().slice(pos);
+ html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
+ html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
+ html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
+ mirror = new Mirror($inputor);
+ return at_rect = mirror.create(html).rect();
+ };
+
+ InputCaret.prototype.getIEPosition = function(pos) {
+ var h, inputorOffset, offset, x, y;
+ offset = this.getIEOffset(pos);
+ inputorOffset = this.$inputor.offset();
+ x = offset.left - inputorOffset.left;
+ y = offset.top - inputorOffset.top;
+ h = offset.height;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ return InputCaret;
+
+})();
+
+Mirror = (function() {
+ Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.mirrorCss = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000
+ };
+ if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+ this.css_attr.push('width');
+ }
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('<div></div>');
+ this.$mirror.css(this.mirrorCss());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("#caret");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ height: $flag.height()
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+})();
+
+Utils = {
+ contentEditable: function($inputor) {
+ return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+ }
+};
+
+methods = {
+ pos: function(pos) {
+ if (pos || pos === 0) {
+ return this.setPos(pos);
+ } else {
+ return this.getPos();
+ }
+ },
+ position: function(pos) {
+ if (oDocument.selection) {
+ return this.getIEPosition(pos);
+ } else {
+ return this.getPosition(pos);
+ }
+ },
+ offset: function(pos) {
+ var offset;
+ offset = this.getOffset(pos);
+ return offset;
+ }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+ var iframe;
+ if (iframe = settings != null ? settings.iframe : void 0) {
+ oFrame = iframe;
+ oWindow = iframe.contentWindow;
+ return oDocument = iframe.contentDocument || oWindow.document;
+ } else {
+ oFrame = void 0;
+ oWindow = window;
+ return oDocument = document;
+ }
+};
+
+discoveryIframeOf = function($dom) {
+ var error;
+ oDocument = $dom[0].ownerDocument;
+ oWindow = oDocument.defaultView || oDocument.parentWindow;
+ try {
+ return oFrame = oWindow.frameElement;
+ } catch (_error) {
+ error = _error;
+ }
+};
+
+$.fn.caret = function(method, value, settings) {
+ var caret;
+ if (methods[method]) {
+ if ($.isPlainObject(value)) {
+ setContextBy(value);
+ value = void 0;
+ } else {
+ setContextBy(settings);
+ }
+ caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+ return methods[method].apply(caret, [value]);
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.caret");
+ }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
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/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js
deleted file mode 100644
index fd6e95e75d5..00000000000
--- a/vendor/assets/javascripts/jquery.turbolinks.js
+++ /dev/null
@@ -1,49 +0,0 @@
-// Generated by CoffeeScript 1.7.1
-
-/*
-jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks
-jQuery plugin for drop-in fix binded events problem caused by Turbolinks
-
-The MIT License
-Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz
- */
-
-(function() {
- var $, $document;
-
- $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0);
-
- $document = $(document);
-
- $.turbo = {
- version: '2.1.0',
- isReady: false,
- use: function(load, fetch) {
- return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch);
- },
- addCallback: function(callback) {
- if ($.turbo.isReady) {
- callback($);
- }
- return $document.on('turbo:ready', function() {
- return callback($);
- });
- },
- onLoad: function() {
- $.turbo.isReady = true;
- return $document.trigger('turbo:ready');
- },
- onFetch: function() {
- return $.turbo.isReady = false;
- },
- register: function() {
- $(this.onLoad);
- return $.fn.ready = this.addCallback;
- }
- };
-
- $.turbo.register();
-
- $.turbo.use('page:load', 'page:fetch');
-
-}).call(this);
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/assets/javascripts/timeago.js b/vendor/assets/javascripts/timeago.js
deleted file mode 100644
index 0eb6f7967a5..00000000000
--- a/vendor/assets/javascripts/timeago.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * Copyright (c) 2016 hustcc
- * License: MIT
- * Version: v2.0.2
- * https://github.com/hustcc/timeago.js
- * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
-**/
-/* eslint-disable */
-/* jshint expr: true */
-!function (root, factory) {
- if (typeof module === 'object' && module.exports)
- module.exports = factory(root);
- else
- root.timeago = factory(root);
-}(typeof window !== 'undefined' ? window : this,
-function () {
- var cnt = 0, // the timer counter, for timer key
- indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
-
- // build-in locales: en & zh_CN
- locales = {
- 'en': function(number, index) {
- if (index === 0) return ['just now', 'right now'];
- var unit = indexMapEn[parseInt(index / 2)];
- if (number > 1) unit += 's';
- return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
- },
- },
- // second, minute, hour, day, week, month, year(365 days)
- SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
- SEC_ARRAY_LEN = 6,
- ATTR_DATETIME = 'datetime';
-
- // format Date / string / timestamp to Date instance.
- function toDate(input) {
- if (input instanceof Date) return input;
- if (!isNaN(input)) return new Date(toInt(input));
- if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
- input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
- .replace(/-/, '/').replace(/-/, '/')
- .replace(/T/, ' ').replace(/Z/, ' UTC')
- .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
- return new Date(input);
- }
- // change f into int, remove Decimal. just for code compression
- function toInt(f) {
- return parseInt(f);
- }
- // format the diff second to *** time ago, with setting locale
- function formatDiff(diff, locale, defaultLocale) {
- // if locale is not exist, use defaultLocale.
- // if defaultLocale is not exist, use build-in `en`.
- // be sure of no error when locale is not exist.
- locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
- // if (! locales[locale]) locale = defaultLocale;
- var i = 0;
- agoin = diff < 0 ? 1 : 0; // timein or timeago
- diff = Math.abs(diff);
-
- for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- diff /= SEC_ARRAY[i];
- }
- diff = toInt(diff);
- i *= 2;
-
- if (diff > (i === 0 ? 9 : 1)) i += 1;
- return locales[locale](diff, i)[agoin].replace('%s', diff);
- }
- // calculate the diff second between date to be formated an now date.
- function diffSec(date, nowDate) {
- nowDate = nowDate ? toDate(nowDate) : new Date();
- return (nowDate - toDate(date)) / 1000;
- }
- /**
- * nextInterval: calculate the next interval time.
- * - diff: the diff sec between now and date to be formated.
- *
- * What's the meaning?
- * diff = 61 then return 59
- * diff = 3601 (an hour + 1 second), then return 3599
- * make the interval with high performace.
- **/
- function nextInterval(diff) {
- var rst = 1, i = 0, d = Math.abs(diff);
- for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- diff /= SEC_ARRAY[i];
- rst *= SEC_ARRAY[i];
- }
- // return leftSec(d, rst);
- d = d % rst;
- d = d ? rst - d : rst;
- return Math.ceil(d);
- }
- // get the datetime attribute, jQuery and DOM
- function getDateAttr(node) {
- if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
- if(node.attr) return node.attr(ATTR_DATETIME);
- }
- /**
- * timeago: the function to get `timeago` instance.
- * - nowDate: the relative date, default is new Date().
- * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- * var timeago = timeagoLib(); // all use default.
- * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
- * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
- * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
- **/
- function Timeago(nowDate, defaultLocale) {
- var timers = {}; // real-time render timers
- // if do not set the defaultLocale, set it with `en`
- if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
- // what the timer will do
- function doRender(node, date, locale, cnt) {
- var diff = diffSec(date, nowDate);
- node.innerHTML = formatDiff(diff, locale, defaultLocale);
- // waiting %s seconds, do the next render
- timers['k' + cnt] = setTimeout(function() {
- doRender(node, date, locale, cnt);
- }, nextInterval(diff) * 1000);
- }
- /**
- * nextInterval: calculate the next interval time.
- * - diff: the diff sec between now and date to be formated.
- *
- * What's the meaning?
- * diff = 61 then return 59
- * diff = 3601 (an hour + 1 second), then return 3599
- * make the interval with high performace.
- **/
- // this.nextInterval = function(diff) { // for dev test
- // var rst = 1, i = 0, d = Math.abs(diff);
- // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- // diff /= SEC_ARRAY[i];
- // rst *= SEC_ARRAY[i];
- // }
- // // return leftSec(d, rst);
- // d = d % rst;
- // d = d ? rst - d : rst;
- // return Math.ceil(d);
- // }; // for dev test
- /**
- * format: format the date to *** time ago, with setting or default locale
- * - date: the date / string / timestamp to be formated
- * - locale: the formated string's locale name, e.g. en / zh_CN
- *
- * How to use it?
- * var timeago = require('timeago.js')();
- * timeago.format(new Date(), 'pl'); // Date instance
- * timeago.format('2016-09-10', 'fr'); // formated date string
- * timeago.format(1473473400269); // timestamp with ms
- **/
- this.format = function(date, locale) {
- return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
- };
- /**
- * render: render the DOM real-time.
- * - nodes: which nodes will be rendered.
- * - locale: the locale name used to format date.
- *
- * How to use it?
- * var timeago = new require('timeago.js')();
- * // 1. javascript selector
- * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
- * // 2. use jQuery selector
- * timeago.render($('.need_to_be_rendered'), 'pl');
- *
- * Notice: please be sure the dom has attribute `datetime`.
- **/
- this.render = function(nodes, locale) {
- if (nodes.length === undefined) nodes = [nodes];
- for (var i = 0; i < nodes.length; i++) {
- doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
- }
- };
- /**
- * cancel: cancel all the timers which are doing real-time render.
- *
- * How to use it?
- * var timeago = new require('timeago.js')();
- * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
- * timeago.cancel(); // will stop all the timer, stop render in real time.
- **/
- this.cancel = function() {
- for (var key in timers) {
- clearTimeout(timers[key]);
- }
- timers = {};
- };
- /**
- * setLocale: set the default locale name.
- *
- * How to use it?
- * var timeago = require('timeago.js');
- * timeago = new timeago();
- * timeago.setLocale('fr');
- **/
- this.setLocale = function(locale) {
- defaultLocale = locale;
- };
- return this;
- }
- /**
- * timeago: the function to get `timeago` instance.
- * - nowDate: the relative date, default is new Date().
- * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- * var timeago = timeagoLib(); // all use default.
- * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
- * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
- * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
- **/
- function timeagoFactory(nowDate, defaultLocale) {
- return new Timeago(nowDate, defaultLocale);
- }
- /**
- * register: register a new language locale
- * - locale: locale name, e.g. en / zh_CN, notice the standard.
- * - localeFunc: the locale process function
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- *
- * timeagoLib.register('the locale name', the_locale_func);
- * // or
- * timeagoLib.register('pl', require('timeago.js/locales/pl'));
- **/
- timeagoFactory.register = function(locale, localeFunc) {
- locales[locale] = localeFunc;
- };
-
- return timeagoFactory;
-});
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
index e666b136051..a33e5e0ade9 100644
--- a/vendor/assets/javascripts/u2f.js
+++ b/vendor/assets/javascripts/u2f.js
@@ -745,4 +745,6 @@ u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
};
port.postMessage(req);
});
-}; \ No newline at end of file
+};
+
+window.u2f || (window.u2f = u2f);
diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js
deleted file mode 100644
index d7981dbec7e..00000000000
--- a/vendor/assets/javascripts/vue-resource.full.js
+++ /dev/null
@@ -1,1318 +0,0 @@
-/*!
- * vue-resource v0.9.3
- * https://github.com/vuejs/vue-resource
- * Released under the MIT License.
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.VueResource = factory());
-}(this, function () { 'use strict';
-
- /**
- * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis)
- */
-
- var RESOLVED = 0;
- var REJECTED = 1;
- var PENDING = 2;
-
- function Promise$2(executor) {
-
- this.state = PENDING;
- this.value = undefined;
- this.deferred = [];
-
- var promise = this;
-
- try {
- executor(function (x) {
- promise.resolve(x);
- }, function (r) {
- promise.reject(r);
- });
- } catch (e) {
- promise.reject(e);
- }
- }
-
- Promise$2.reject = function (r) {
- return new Promise$2(function (resolve, reject) {
- reject(r);
- });
- };
-
- Promise$2.resolve = function (x) {
- return new Promise$2(function (resolve, reject) {
- resolve(x);
- });
- };
-
- Promise$2.all = function all(iterable) {
- return new Promise$2(function (resolve, reject) {
- var count = 0,
- result = [];
-
- if (iterable.length === 0) {
- resolve(result);
- }
-
- function resolver(i) {
- return function (x) {
- result[i] = x;
- count += 1;
-
- if (count === iterable.length) {
- resolve(result);
- }
- };
- }
-
- for (var i = 0; i < iterable.length; i += 1) {
- Promise$2.resolve(iterable[i]).then(resolver(i), reject);
- }
- });
- };
-
- Promise$2.race = function race(iterable) {
- return new Promise$2(function (resolve, reject) {
- for (var i = 0; i < iterable.length; i += 1) {
- Promise$2.resolve(iterable[i]).then(resolve, reject);
- }
- });
- };
-
- var p$1 = Promise$2.prototype;
-
- p$1.resolve = function resolve(x) {
- var promise = this;
-
- if (promise.state === PENDING) {
- if (x === promise) {
- throw new TypeError('Promise settled with itself.');
- }
-
- var called = false;
-
- try {
- var then = x && x['then'];
-
- if (x !== null && typeof x === 'object' && typeof then === 'function') {
- then.call(x, function (x) {
- if (!called) {
- promise.resolve(x);
- }
- called = true;
- }, function (r) {
- if (!called) {
- promise.reject(r);
- }
- called = true;
- });
- return;
- }
- } catch (e) {
- if (!called) {
- promise.reject(e);
- }
- return;
- }
-
- promise.state = RESOLVED;
- promise.value = x;
- promise.notify();
- }
- };
-
- p$1.reject = function reject(reason) {
- var promise = this;
-
- if (promise.state === PENDING) {
- if (reason === promise) {
- throw new TypeError('Promise settled with itself.');
- }
-
- promise.state = REJECTED;
- promise.value = reason;
- promise.notify();
- }
- };
-
- p$1.notify = function notify() {
- var promise = this;
-
- nextTick(function () {
- if (promise.state !== PENDING) {
- while (promise.deferred.length) {
- var deferred = promise.deferred.shift(),
- onResolved = deferred[0],
- onRejected = deferred[1],
- resolve = deferred[2],
- reject = deferred[3];
-
- try {
- if (promise.state === RESOLVED) {
- if (typeof onResolved === 'function') {
- resolve(onResolved.call(undefined, promise.value));
- } else {
- resolve(promise.value);
- }
- } else if (promise.state === REJECTED) {
- if (typeof onRejected === 'function') {
- resolve(onRejected.call(undefined, promise.value));
- } else {
- reject(promise.value);
- }
- }
- } catch (e) {
- reject(e);
- }
- }
- }
- });
- };
-
- p$1.then = function then(onResolved, onRejected) {
- var promise = this;
-
- return new Promise$2(function (resolve, reject) {
- promise.deferred.push([onResolved, onRejected, resolve, reject]);
- promise.notify();
- });
- };
-
- p$1.catch = function (onRejected) {
- return this.then(undefined, onRejected);
- };
-
- var PromiseObj = window.Promise || Promise$2;
-
- function Promise$1(executor, context) {
-
- if (executor instanceof PromiseObj) {
- this.promise = executor;
- } else {
- this.promise = new PromiseObj(executor.bind(context));
- }
-
- this.context = context;
- }
-
- Promise$1.all = function (iterable, context) {
- return new Promise$1(PromiseObj.all(iterable), context);
- };
-
- Promise$1.resolve = function (value, context) {
- return new Promise$1(PromiseObj.resolve(value), context);
- };
-
- Promise$1.reject = function (reason, context) {
- return new Promise$1(PromiseObj.reject(reason), context);
- };
-
- Promise$1.race = function (iterable, context) {
- return new Promise$1(PromiseObj.race(iterable), context);
- };
-
- var p = Promise$1.prototype;
-
- p.bind = function (context) {
- this.context = context;
- return this;
- };
-
- p.then = function (fulfilled, rejected) {
-
- if (fulfilled && fulfilled.bind && this.context) {
- fulfilled = fulfilled.bind(this.context);
- }
-
- if (rejected && rejected.bind && this.context) {
- rejected = rejected.bind(this.context);
- }
-
- return new Promise$1(this.promise.then(fulfilled, rejected), this.context);
- };
-
- p.catch = function (rejected) {
-
- if (rejected && rejected.bind && this.context) {
- rejected = rejected.bind(this.context);
- }
-
- return new Promise$1(this.promise.catch(rejected), this.context);
- };
-
- p.finally = function (callback) {
-
- return this.then(function (value) {
- callback.call(this);
- return value;
- }, function (reason) {
- callback.call(this);
- return PromiseObj.reject(reason);
- });
- };
-
- var debug = false;
- var util = {};
- var array = [];
- function Util (Vue) {
- util = Vue.util;
- debug = Vue.config.debug || !Vue.config.silent;
- }
-
- function warn(msg) {
- if (typeof console !== 'undefined' && debug) {
- console.warn('[VueResource warn]: ' + msg);
- }
- }
-
- function error(msg) {
- if (typeof console !== 'undefined') {
- console.error(msg);
- }
- }
-
- function nextTick(cb, ctx) {
- return util.nextTick(cb, ctx);
- }
-
- function trim(str) {
- return str.replace(/^\s*|\s*$/g, '');
- }
-
- var isArray = Array.isArray;
-
- function isString(val) {
- return typeof val === 'string';
- }
-
- function isBoolean(val) {
- return val === true || val === false;
- }
-
- function isFunction(val) {
- return typeof val === 'function';
- }
-
- function isObject(obj) {
- return obj !== null && typeof obj === 'object';
- }
-
- function isPlainObject(obj) {
- return isObject(obj) && Object.getPrototypeOf(obj) == Object.prototype;
- }
-
- function isFormData(obj) {
- return typeof FormData !== 'undefined' && obj instanceof FormData;
- }
-
- function when(value, fulfilled, rejected) {
-
- var promise = Promise$1.resolve(value);
-
- if (arguments.length < 2) {
- return promise;
- }
-
- return promise.then(fulfilled, rejected);
- }
-
- function options(fn, obj, opts) {
-
- opts = opts || {};
-
- if (isFunction(opts)) {
- opts = opts.call(obj);
- }
-
- return merge(fn.bind({ $vm: obj, $options: opts }), fn, { $options: opts });
- }
-
- function each(obj, iterator) {
-
- var i, key;
-
- if (typeof obj.length == 'number') {
- for (i = 0; i < obj.length; i++) {
- iterator.call(obj[i], obj[i], i);
- }
- } else if (isObject(obj)) {
- for (key in obj) {
- if (obj.hasOwnProperty(key)) {
- iterator.call(obj[key], obj[key], key);
- }
- }
- }
-
- return obj;
- }
-
- var assign = Object.assign || _assign;
-
- function merge(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
- _merge(target, source, true);
- });
-
- return target;
- }
-
- function defaults(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
-
- for (var key in source) {
- if (target[key] === undefined) {
- target[key] = source[key];
- }
- }
- });
-
- return target;
- }
-
- function _assign(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
- _merge(target, source);
- });
-
- return target;
- }
-
- function _merge(target, source, deep) {
- for (var key in source) {
- if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
- if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
- target[key] = {};
- }
- if (isArray(source[key]) && !isArray(target[key])) {
- target[key] = [];
- }
- _merge(target[key], source[key], deep);
- } else if (source[key] !== undefined) {
- target[key] = source[key];
- }
- }
- }
-
- function root (options, next) {
-
- var url = next(options);
-
- if (isString(options.root) && !url.match(/^(https?:)?\//)) {
- url = options.root + '/' + url;
- }
-
- return url;
- }
-
- function query (options, next) {
-
- var urlParams = Object.keys(Url.options.params),
- query = {},
- url = next(options);
-
- each(options.params, function (value, key) {
- if (urlParams.indexOf(key) === -1) {
- query[key] = value;
- }
- });
-
- query = Url.params(query);
-
- if (query) {
- url += (url.indexOf('?') == -1 ? '?' : '&') + query;
- }
-
- return url;
- }
-
- /**
- * URL Template v2.0.6 (https://github.com/bramstein/url-template)
- */
-
- function expand(url, params, variables) {
-
- var tmpl = parse(url),
- expanded = tmpl.expand(params);
-
- if (variables) {
- variables.push.apply(variables, tmpl.vars);
- }
-
- return expanded;
- }
-
- function parse(template) {
-
- var operators = ['+', '#', '.', '/', ';', '?', '&'],
- variables = [];
-
- return {
- vars: variables,
- expand: function (context) {
- return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
- if (expression) {
-
- var operator = null,
- values = [];
-
- if (operators.indexOf(expression.charAt(0)) !== -1) {
- operator = expression.charAt(0);
- expression = expression.substr(1);
- }
-
- expression.split(/,/g).forEach(function (variable) {
- var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
- values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
- variables.push(tmp[1]);
- });
-
- if (operator && operator !== '+') {
-
- var separator = ',';
-
- if (operator === '?') {
- separator = '&';
- } else if (operator !== '#') {
- separator = operator;
- }
-
- return (values.length !== 0 ? operator : '') + values.join(separator);
- } else {
- return values.join(',');
- }
- } else {
- return encodeReserved(literal);
- }
- });
- }
- };
- }
-
- function getValues(context, operator, key, modifier) {
-
- var value = context[key],
- result = [];
-
- if (isDefined(value) && value !== '') {
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- value = value.toString();
-
- if (modifier && modifier !== '*') {
- value = value.substring(0, parseInt(modifier, 10));
- }
-
- result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
- } else {
- if (modifier === '*') {
- if (Array.isArray(value)) {
- value.filter(isDefined).forEach(function (value) {
- result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
- });
- } else {
- Object.keys(value).forEach(function (k) {
- if (isDefined(value[k])) {
- result.push(encodeValue(operator, value[k], k));
- }
- });
- }
- } else {
- var tmp = [];
-
- if (Array.isArray(value)) {
- value.filter(isDefined).forEach(function (value) {
- tmp.push(encodeValue(operator, value));
- });
- } else {
- Object.keys(value).forEach(function (k) {
- if (isDefined(value[k])) {
- tmp.push(encodeURIComponent(k));
- tmp.push(encodeValue(operator, value[k].toString()));
- }
- });
- }
-
- if (isKeyOperator(operator)) {
- result.push(encodeURIComponent(key) + '=' + tmp.join(','));
- } else if (tmp.length !== 0) {
- result.push(tmp.join(','));
- }
- }
- }
- } else {
- if (operator === ';') {
- result.push(encodeURIComponent(key));
- } else if (value === '' && (operator === '&' || operator === '?')) {
- result.push(encodeURIComponent(key) + '=');
- } else if (value === '') {
- result.push('');
- }
- }
-
- return result;
- }
-
- function isDefined(value) {
- return value !== undefined && value !== null;
- }
-
- function isKeyOperator(operator) {
- return operator === ';' || operator === '&' || operator === '?';
- }
-
- function encodeValue(operator, value, key) {
-
- value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeURIComponent(value);
-
- if (key) {
- return encodeURIComponent(key) + '=' + value;
- } else {
- return value;
- }
- }
-
- function encodeReserved(str) {
- return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) {
- if (!/%[0-9A-Fa-f]/.test(part)) {
- part = encodeURI(part);
- }
- return part;
- }).join('');
- }
-
- function template (options) {
-
- var variables = [],
- url = expand(options.url, options.params, variables);
-
- variables.forEach(function (key) {
- delete options.params[key];
- });
-
- return url;
- }
-
- /**
- * Service for URL templating.
- */
-
- var ie = document.documentMode;
- var el = document.createElement('a');
-
- function Url(url, params) {
-
- var self = this || {},
- options = url,
- transform;
-
- if (isString(url)) {
- options = { url: url, params: params };
- }
-
- options = merge({}, Url.options, self.$options, options);
-
- Url.transforms.forEach(function (handler) {
- transform = factory(handler, transform, self.$vm);
- });
-
- return transform(options);
- }
-
- /**
- * Url options.
- */
-
- Url.options = {
- url: '',
- root: null,
- params: {}
- };
-
- /**
- * Url transforms.
- */
-
- Url.transforms = [template, query, root];
-
- /**
- * Encodes a Url parameter string.
- *
- * @param {Object} obj
- */
-
- Url.params = function (obj) {
-
- var params = [],
- escape = encodeURIComponent;
-
- params.add = function (key, value) {
-
- if (isFunction(value)) {
- value = value();
- }
-
- if (value === null) {
- value = '';
- }
-
- this.push(escape(key) + '=' + escape(value));
- };
-
- serialize(params, obj);
-
- return params.join('&').replace(/%20/g, '+');
- };
-
- /**
- * Parse a URL and return its components.
- *
- * @param {String} url
- */
-
- Url.parse = function (url) {
-
- if (ie) {
- el.href = url;
- url = el.href;
- }
-
- el.href = url;
-
- return {
- href: el.href,
- protocol: el.protocol ? el.protocol.replace(/:$/, '') : '',
- port: el.port,
- host: el.host,
- hostname: el.hostname,
- pathname: el.pathname.charAt(0) === '/' ? el.pathname : '/' + el.pathname,
- search: el.search ? el.search.replace(/^\?/, '') : '',
- hash: el.hash ? el.hash.replace(/^#/, '') : ''
- };
- };
-
- function factory(handler, next, vm) {
- return function (options) {
- return handler.call(vm, options, next);
- };
- }
-
- function serialize(params, obj, scope) {
-
- var array = isArray(obj),
- plain = isPlainObject(obj),
- hash;
-
- each(obj, function (value, key) {
-
- hash = isObject(value) || isArray(value);
-
- if (scope) {
- key = scope + '[' + (plain || hash ? key : '') + ']';
- }
-
- if (!scope && array) {
- params.add(value.name, value.value);
- } else if (hash) {
- serialize(params, value, key);
- } else {
- params.add(key, value);
- }
- });
- }
-
- function xdrClient (request) {
- return new Promise$1(function (resolve) {
-
- var xdr = new XDomainRequest(),
- handler = function (event) {
-
- var response = request.respondWith(xdr.responseText, {
- status: xdr.status,
- statusText: xdr.statusText
- });
-
- resolve(response);
- };
-
- request.abort = function () {
- return xdr.abort();
- };
-
- xdr.open(request.method, request.getUrl(), true);
- xdr.timeout = 0;
- xdr.onload = handler;
- xdr.onerror = handler;
- xdr.ontimeout = function () {};
- xdr.onprogress = function () {};
- xdr.send(request.getBody());
- });
- }
-
- var ORIGIN_URL = Url.parse(location.href);
- var SUPPORTS_CORS = 'withCredentials' in new XMLHttpRequest();
-
- function cors (request, next) {
-
- if (!isBoolean(request.crossOrigin) && crossOrigin(request)) {
- request.crossOrigin = true;
- }
-
- if (request.crossOrigin) {
-
- if (!SUPPORTS_CORS) {
- request.client = xdrClient;
- }
-
- delete request.emulateHTTP;
- }
-
- next();
- }
-
- function crossOrigin(request) {
-
- var requestUrl = Url.parse(Url(request));
-
- return requestUrl.protocol !== ORIGIN_URL.protocol || requestUrl.host !== ORIGIN_URL.host;
- }
-
- function body (request, next) {
-
- if (request.emulateJSON && isPlainObject(request.body)) {
- request.body = Url.params(request.body);
- request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
- if (isFormData(request.body)) {
- delete request.headers['Content-Type'];
- }
-
- if (isPlainObject(request.body)) {
- request.body = JSON.stringify(request.body);
- }
-
- next(function (response) {
-
- var contentType = response.headers['Content-Type'];
-
- if (isString(contentType) && contentType.indexOf('application/json') === 0) {
-
- try {
- response.data = response.json();
- } catch (e) {
- response.data = null;
- }
- } else {
- response.data = response.text();
- }
- });
- }
-
- function jsonpClient (request) {
- return new Promise$1(function (resolve) {
-
- var name = request.jsonp || 'callback',
- callback = '_jsonp' + Math.random().toString(36).substr(2),
- body = null,
- handler,
- script;
-
- handler = function (event) {
-
- var status = 0;
-
- if (event.type === 'load' && body !== null) {
- status = 200;
- } else if (event.type === 'error') {
- status = 404;
- }
-
- resolve(request.respondWith(body, { status: status }));
-
- delete window[callback];
- document.body.removeChild(script);
- };
-
- request.params[name] = callback;
-
- window[callback] = function (result) {
- body = JSON.stringify(result);
- };
-
- script = document.createElement('script');
- script.src = request.getUrl();
- script.type = 'text/javascript';
- script.async = true;
- script.onload = handler;
- script.onerror = handler;
-
- document.body.appendChild(script);
- });
- }
-
- function jsonp (request, next) {
-
- if (request.method == 'JSONP') {
- request.client = jsonpClient;
- }
-
- next(function (response) {
-
- if (request.method == 'JSONP') {
- response.data = response.json();
- }
- });
- }
-
- function before (request, next) {
-
- if (isFunction(request.before)) {
- request.before.call(this, request);
- }
-
- next();
- }
-
- /**
- * HTTP method override Interceptor.
- */
-
- function method (request, next) {
-
- if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) {
- request.headers['X-HTTP-Method-Override'] = request.method;
- request.method = 'POST';
- }
-
- next();
- }
-
- function header (request, next) {
-
- request.method = request.method.toUpperCase();
- request.headers = assign({}, Http.headers.common, !request.crossOrigin ? Http.headers.custom : {}, Http.headers[request.method.toLowerCase()], request.headers);
-
- next();
- }
-
- /**
- * Timeout Interceptor.
- */
-
- function timeout (request, next) {
-
- var timeout;
-
- if (request.timeout) {
- timeout = setTimeout(function () {
- request.abort();
- }, request.timeout);
- }
-
- next(function (response) {
-
- clearTimeout(timeout);
- });
- }
-
- function xhrClient (request) {
- return new Promise$1(function (resolve) {
-
- var xhr = new XMLHttpRequest(),
- handler = function (event) {
-
- var response = request.respondWith('response' in xhr ? xhr.response : xhr.responseText, {
- status: xhr.status === 1223 ? 204 : xhr.status, // IE9 status bug
- statusText: xhr.status === 1223 ? 'No Content' : trim(xhr.statusText),
- headers: parseHeaders(xhr.getAllResponseHeaders())
- });
-
- resolve(response);
- };
-
- request.abort = function () {
- return xhr.abort();
- };
-
- xhr.open(request.method, request.getUrl(), true);
- xhr.timeout = 0;
- xhr.onload = handler;
- xhr.onerror = handler;
-
- if (request.progress) {
- if (request.method === 'GET') {
- xhr.addEventListener('progress', request.progress);
- } else if (/^(POST|PUT)$/i.test(request.method)) {
- xhr.upload.addEventListener('progress', request.progress);
- }
- }
-
- if (request.credentials === true) {
- xhr.withCredentials = true;
- }
-
- each(request.headers || {}, function (value, header) {
- xhr.setRequestHeader(header, value);
- });
-
- xhr.send(request.getBody());
- });
- }
-
- function parseHeaders(str) {
-
- var headers = {},
- value,
- name,
- i;
-
- each(trim(str).split('\n'), function (row) {
-
- i = row.indexOf(':');
- name = trim(row.slice(0, i));
- value = trim(row.slice(i + 1));
-
- if (headers[name]) {
-
- if (isArray(headers[name])) {
- headers[name].push(value);
- } else {
- headers[name] = [headers[name], value];
- }
- } else {
-
- headers[name] = value;
- }
- });
-
- return headers;
- }
-
- function Client (context) {
-
- var reqHandlers = [sendRequest],
- resHandlers = [],
- handler;
-
- if (!isObject(context)) {
- context = null;
- }
-
- function Client(request) {
- return new Promise$1(function (resolve) {
-
- function exec() {
-
- handler = reqHandlers.pop();
-
- if (isFunction(handler)) {
- handler.call(context, request, next);
- } else {
- warn('Invalid interceptor of type ' + typeof handler + ', must be a function');
- next();
- }
- }
-
- function next(response) {
-
- if (isFunction(response)) {
-
- resHandlers.unshift(response);
- } else if (isObject(response)) {
-
- resHandlers.forEach(function (handler) {
- response = when(response, function (response) {
- return handler.call(context, response) || response;
- });
- });
-
- when(response, resolve);
-
- return;
- }
-
- exec();
- }
-
- exec();
- }, context);
- }
-
- Client.use = function (handler) {
- reqHandlers.push(handler);
- };
-
- return Client;
- }
-
- function sendRequest(request, resolve) {
-
- var client = request.client || xhrClient;
-
- resolve(client(request));
- }
-
- var classCallCheck = function (instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- /**
- * HTTP Response.
- */
-
- var Response = function () {
- function Response(body, _ref) {
- var url = _ref.url;
- var headers = _ref.headers;
- var status = _ref.status;
- var statusText = _ref.statusText;
- classCallCheck(this, Response);
-
-
- this.url = url;
- this.body = body;
- this.headers = headers || {};
- this.status = status || 0;
- this.statusText = statusText || '';
- this.ok = status >= 200 && status < 300;
- }
-
- Response.prototype.text = function text() {
- return this.body;
- };
-
- Response.prototype.blob = function blob() {
- return new Blob([this.body]);
- };
-
- Response.prototype.json = function json() {
- return JSON.parse(this.body);
- };
-
- return Response;
- }();
-
- var Request = function () {
- function Request(options) {
- classCallCheck(this, Request);
-
-
- this.method = 'GET';
- this.body = null;
- this.params = {};
- this.headers = {};
-
- assign(this, options);
- }
-
- Request.prototype.getUrl = function getUrl() {
- return Url(this);
- };
-
- Request.prototype.getBody = function getBody() {
- return this.body;
- };
-
- Request.prototype.respondWith = function respondWith(body, options) {
- return new Response(body, assign(options || {}, { url: this.getUrl() }));
- };
-
- return Request;
- }();
-
- /**
- * Service for sending network requests.
- */
-
- var CUSTOM_HEADERS = { 'X-Requested-With': 'XMLHttpRequest' };
- var COMMON_HEADERS = { 'Accept': 'application/json, text/plain, */*' };
- var JSON_CONTENT_TYPE = { 'Content-Type': 'application/json;charset=utf-8' };
-
- function Http(options) {
-
- var self = this || {},
- client = Client(self.$vm);
-
- defaults(options || {}, self.$options, Http.options);
-
- Http.interceptors.forEach(function (handler) {
- client.use(handler);
- });
-
- return client(new Request(options)).then(function (response) {
-
- return response.ok ? response : Promise$1.reject(response);
- }, function (response) {
-
- if (response instanceof Error) {
- error(response);
- }
-
- return Promise$1.reject(response);
- });
- }
-
- Http.options = {};
-
- Http.headers = {
- put: JSON_CONTENT_TYPE,
- post: JSON_CONTENT_TYPE,
- patch: JSON_CONTENT_TYPE,
- delete: JSON_CONTENT_TYPE,
- custom: CUSTOM_HEADERS,
- common: COMMON_HEADERS
- };
-
- Http.interceptors = [before, timeout, method, body, jsonp, header, cors];
-
- ['get', 'delete', 'head', 'jsonp'].forEach(function (method) {
-
- Http[method] = function (url, options) {
- return this(assign(options || {}, { url: url, method: method }));
- };
- });
-
- ['post', 'put', 'patch'].forEach(function (method) {
-
- Http[method] = function (url, body, options) {
- return this(assign(options || {}, { url: url, method: method, body: body }));
- };
- });
-
- function Resource(url, params, actions, options) {
-
- var self = this || {},
- resource = {};
-
- actions = assign({}, Resource.actions, actions);
-
- each(actions, function (action, name) {
-
- action = merge({ url: url, params: params || {} }, options, action);
-
- resource[name] = function () {
- return (self.$http || Http)(opts(action, arguments));
- };
- });
-
- return resource;
- }
-
- function opts(action, args) {
-
- var options = assign({}, action),
- params = {},
- body;
-
- switch (args.length) {
-
- case 2:
-
- params = args[0];
- body = args[1];
-
- break;
-
- case 1:
-
- if (/^(POST|PUT|PATCH)$/i.test(options.method)) {
- body = args[0];
- } else {
- params = args[0];
- }
-
- break;
-
- case 0:
-
- break;
-
- default:
-
- throw 'Expected up to 4 arguments [params, body], got ' + args.length + ' arguments';
- }
-
- options.body = body;
- options.params = assign({}, options.params, params);
-
- return options;
- }
-
- Resource.actions = {
-
- get: { method: 'GET' },
- save: { method: 'POST' },
- query: { method: 'GET' },
- update: { method: 'PUT' },
- remove: { method: 'DELETE' },
- delete: { method: 'DELETE' }
-
- };
-
- function plugin(Vue) {
-
- if (plugin.installed) {
- return;
- }
-
- Util(Vue);
-
- Vue.url = Url;
- Vue.http = Http;
- Vue.resource = Resource;
- Vue.Promise = Promise$1;
-
- Object.defineProperties(Vue.prototype, {
-
- $url: {
- get: function () {
- return options(Vue.url, this, this.$options.url);
- }
- },
-
- $http: {
- get: function () {
- return options(Vue.http, this, this.$options.http);
- }
- },
-
- $resource: {
- get: function () {
- return Vue.resource.bind(this);
- }
- },
-
- $promise: {
- get: function () {
- var _this = this;
-
- return function (executor) {
- return new Vue.Promise(executor, _this);
- };
- }
- }
-
- });
- }
-
- if (typeof window !== 'undefined' && window.Vue) {
- window.Vue.use(plugin);
- }
-
- return plugin;
-
-})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue-resource.js.erb b/vendor/assets/javascripts/vue-resource.js.erb
deleted file mode 100644
index 8001775ce98..00000000000
--- a/vendor/assets/javascripts/vue-resource.js.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% type = Rails.env.development? ? 'full' : 'min' %>
-<%= File.read(Rails.root.join("vendor/assets/javascripts/vue-resource.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue-resource.min.js b/vendor/assets/javascripts/vue-resource.min.js
deleted file mode 100644
index 6bff73a2a67..00000000000
--- a/vendor/assets/javascripts/vue-resource.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * vue-resource v0.9.3
- * https://github.com/vuejs/vue-resource
- * Released under the MIT License.
- */
-
-!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=Z,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(e){n.reject(e)}}function n(t,n){t instanceof nt?this.promise=t:this.promise=new nt(t.bind(n)),this.context=n}function e(t){rt=t.util,ot=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ot&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return rt.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return"string"==typeof t}function c(t){return t===!0||t===!1}function a(t){return"function"==typeof t}function f(t){return null!==t&&"object"==typeof t}function h(t){return f(t)&&Object.getPrototypeOf(t)==Object.prototype}function p(t){return"undefined"!=typeof FormData&&t instanceof FormData}function l(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function d(t,n,e){return e=e||{},a(e)&&(e=e.call(n)),v(t.bind({$vm:n,$options:e}),t,{$options:e})}function m(t,n){var e,o;if("number"==typeof t.length)for(e=0;e<t.length;e++)n.call(t[e],t[e],e);else if(f(t))for(o in t)t.hasOwnProperty(o)&&n.call(t[o],t[o],o);return t}function v(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n,!0)}),t}function y(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){for(var e in n)void 0===t[e]&&(t[e]=n[e])}),t}function b(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n)}),t}function g(t,n,e){for(var o in n)e&&(h(n[o])||ut(n[o]))?(h(n[o])&&!h(t[o])&&(t[o]={}),ut(n[o])&&!ut(t[o])&&(t[o]=[]),g(t[o],n[o],e)):void 0!==n[o]&&(t[o]=n[o])}function w(t,n){var e=n(t);return s(t.root)&&!e.match(/^(https?:)?\//)&&(e=t.root+"/"+e),e}function T(t,n){var e=Object.keys(R.options.params),o={},r=n(t);return m(t.params,function(t,n){e.indexOf(n)===-1&&(o[n]=t)}),o=R.params(o),o&&(r+=(r.indexOf("?")==-1?"?":"&")+o),r}function j(t,n,e){var o=E(t),r=o.expand(n);return e&&e.push.apply(e,o.vars),r}function E(t){var n=["+","#",".","/",";","?","&"],e=[];return{vars:e,expand:function(o){return t.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g,function(t,r,i){if(r){var u=null,s=[];if(n.indexOf(r.charAt(0))!==-1&&(u=r.charAt(0),r=r.substr(1)),r.split(/,/g).forEach(function(t){var n=/([^:\*]*)(?::(\d+)|(\*))?/.exec(t);s.push.apply(s,x(o,u,n[1],n[2]||n[3])),e.push(n[1])}),u&&"+"!==u){var c=",";return"?"===u?c="&":"#"!==u&&(c=u),(0!==s.length?u:"")+s.join(c)}return s.join(",")}return $(i)})}}}function x(t,n,e,o){var r=t[e],i=[];if(O(r)&&""!==r)if("string"==typeof r||"number"==typeof r||"boolean"==typeof r)r=r.toString(),o&&"*"!==o&&(r=r.substring(0,parseInt(o,10))),i.push(C(n,r,P(n)?e:null));else if("*"===o)Array.isArray(r)?r.filter(O).forEach(function(t){i.push(C(n,t,P(n)?e:null))}):Object.keys(r).forEach(function(t){O(r[t])&&i.push(C(n,r[t],t))});else{var u=[];Array.isArray(r)?r.filter(O).forEach(function(t){u.push(C(n,t))}):Object.keys(r).forEach(function(t){O(r[t])&&(u.push(encodeURIComponent(t)),u.push(C(n,r[t].toString())))}),P(n)?i.push(encodeURIComponent(e)+"="+u.join(",")):0!==u.length&&i.push(u.join(","))}else";"===n?i.push(encodeURIComponent(e)):""!==r||"&"!==n&&"?"!==n?""===r&&i.push(""):i.push(encodeURIComponent(e)+"=");return i}function O(t){return void 0!==t&&null!==t}function P(t){return";"===t||"&"===t||"?"===t}function C(t,n,e){return n="+"===t||"#"===t?$(n):encodeURIComponent(n),e?encodeURIComponent(e)+"="+n:n}function $(t){return t.split(/(%[0-9A-Fa-f]{2})/g).map(function(t){return/%[0-9A-Fa-f]/.test(t)||(t=encodeURI(t)),t}).join("")}function U(t){var n=[],e=j(t.url,t.params,n);return n.forEach(function(n){delete t.params[n]}),e}function R(t,n){var e,o=this||{},r=t;return s(t)&&(r={url:t,params:n}),r=v({},R.options,o.$options,r),R.transforms.forEach(function(t){e=A(t,e,o.$vm)}),e(r)}function A(t,n,e){return function(o){return t.call(e,o,n)}}function S(t,n,e){var o,r=ut(n),i=h(n);m(n,function(n,u){o=f(n)||ut(n),e&&(u=e+"["+(i||o?u:"")+"]"),!e&&r?t.add(n.name,n.value):o?S(t,n,u):t.add(u,n)})}function k(t){return new n(function(n){var e=new XDomainRequest,o=function(o){var r=t.respondWith(e.responseText,{status:e.status,statusText:e.statusText});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,e.ontimeout=function(){},e.onprogress=function(){},e.send(t.getBody())})}function H(t,n){!c(t.crossOrigin)&&I(t)&&(t.crossOrigin=!0),t.crossOrigin&&(ht||(t.client=k),delete t.emulateHTTP),n()}function I(t){var n=R.parse(R(t));return n.protocol!==ft.protocol||n.host!==ft.host}function L(t,n){t.emulateJSON&&h(t.body)&&(t.body=R.params(t.body),t.headers["Content-Type"]="application/x-www-form-urlencoded"),p(t.body)&&delete t.headers["Content-Type"],h(t.body)&&(t.body=JSON.stringify(t.body)),n(function(t){var n=t.headers["Content-Type"];if(s(n)&&0===n.indexOf("application/json"))try{t.data=t.json()}catch(e){t.data=null}else t.data=t.text()})}function q(t){return new n(function(n){var e,o,r=t.jsonp||"callback",i="_jsonp"+Math.random().toString(36).substr(2),u=null;e=function(e){var r=0;"load"===e.type&&null!==u?r=200:"error"===e.type&&(r=404),n(t.respondWith(u,{status:r})),delete window[i],document.body.removeChild(o)},t.params[r]=i,window[i]=function(t){u=JSON.stringify(t)},o=document.createElement("script"),o.src=t.getUrl(),o.type="text/javascript",o.async=!0,o.onload=e,o.onerror=e,document.body.appendChild(o)})}function N(t,n){"JSONP"==t.method&&(t.client=q),n(function(n){"JSONP"==t.method&&(n.data=n.json())})}function D(t,n){a(t.before)&&t.before.call(this,t),n()}function J(t,n){t.emulateHTTP&&/^(PUT|PATCH|DELETE)$/i.test(t.method)&&(t.headers["X-HTTP-Method-Override"]=t.method,t.method="POST"),n()}function M(t,n){t.method=t.method.toUpperCase(),t.headers=st({},V.headers.common,t.crossOrigin?{}:V.headers.custom,V.headers[t.method.toLowerCase()],t.headers),n()}function X(t,n){var e;t.timeout&&(e=setTimeout(function(){t.abort()},t.timeout)),n(function(t){clearTimeout(e)})}function W(t){return new n(function(n){var e=new XMLHttpRequest,o=function(o){var r=t.respondWith("response"in e?e.response:e.responseText,{status:1223===e.status?204:e.status,statusText:1223===e.status?"No Content":u(e.statusText),headers:B(e.getAllResponseHeaders())});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,t.progress&&("GET"===t.method?e.addEventListener("progress",t.progress):/^(POST|PUT)$/i.test(t.method)&&e.upload.addEventListener("progress",t.progress)),t.credentials===!0&&(e.withCredentials=!0),m(t.headers||{},function(t,n){e.setRequestHeader(n,t)}),e.send(t.getBody())})}function B(t){var n,e,o,r={};return m(u(t).split("\n"),function(t){o=t.indexOf(":"),e=u(t.slice(0,o)),n=u(t.slice(o+1)),r[e]?ut(r[e])?r[e].push(n):r[e]=[r[e],n]:r[e]=n}),r}function F(t){function e(e){return new n(function(n){function s(){r=i.pop(),a(r)?r.call(t,e,c):(o("Invalid interceptor of type "+typeof r+", must be a function"),c())}function c(e){if(a(e))u.unshift(e);else if(f(e))return u.forEach(function(n){e=l(e,function(e){return n.call(t,e)||e})}),void l(e,n);s()}s()},t)}var r,i=[G],u=[];return f(t)||(t=null),e.use=function(t){i.push(t)},e}function G(t,n){var e=t.client||W;n(e(t))}function V(t){var e=this||{},o=F(e.$vm);return y(t||{},e.$options,V.options),V.interceptors.forEach(function(t){o.use(t)}),o(new dt(t)).then(function(t){return t.ok?t:n.reject(t)},function(t){return t instanceof Error&&r(t),n.reject(t)})}function _(t,n,e,o){var r=this||{},i={};return e=st({},_.actions,e),m(e,function(e,u){e=v({url:t,params:n||{}},o,e),i[u]=function(){return(r.$http||V)(z(e,arguments))}}),i}function z(t,n){var e,o=st({},t),r={};switch(n.length){case 2:r=n[0],e=n[1];break;case 1:/^(POST|PUT|PATCH)$/i.test(o.method)?e=n[0]:r=n[0];break;case 0:break;default:throw"Expected up to 4 arguments [params, body], got "+n.length+" arguments"}return o.body=e,o.params=st({},o.params,r),o}function K(t){K.installed||(e(t),t.url=R,t.http=V,t.resource=_,t.Promise=n,Object.defineProperties(t.prototype,{$url:{get:function(){return d(t.url,this,this.$options.url)}},$http:{get:function(){return d(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}},$promise:{get:function(){var n=this;return function(e){return new t.Promise(e,n)}}}}))}var Q=0,Y=1,Z=2;t.reject=function(n){return new t(function(t,e){e(n)})},t.resolve=function(n){return new t(function(t,e){t(n)})},t.all=function(n){return new t(function(e,o){function r(t){return function(o){u[t]=o,i+=1,i===n.length&&e(u)}}var i=0,u=[];0===n.length&&e(u);for(var s=0;s<n.length;s+=1)t.resolve(n[s]).then(r(s),o)})},t.race=function(n){return new t(function(e,o){for(var r=0;r<n.length;r+=1)t.resolve(n[r]).then(e,o)})};var tt=t.prototype;tt.resolve=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");var e=!1;try{var o=t&&t.then;if(null!==t&&"object"==typeof t&&"function"==typeof o)return void o.call(t,function(t){e||n.resolve(t),e=!0},function(t){e||n.reject(t),e=!0})}catch(r){return void(e||n.reject(r))}n.state=Q,n.value=t,n.notify()}},tt.reject=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");n.state=Y,n.value=t,n.notify()}},tt.notify=function(){var t=this;i(function(){if(t.state!==Z)for(;t.deferred.length;){var n=t.deferred.shift(),e=n[0],o=n[1],r=n[2],i=n[3];try{t.state===Q?r("function"==typeof e?e.call(void 0,t.value):t.value):t.state===Y&&("function"==typeof o?r(o.call(void 0,t.value)):i(t.value))}catch(u){i(u)}}})},tt.then=function(n,e){var o=this;return new t(function(t,r){o.deferred.push([n,e,t,r]),o.notify()})},tt["catch"]=function(t){return this.then(void 0,t)};var nt=window.Promise||t;n.all=function(t,e){return new n(nt.all(t),e)},n.resolve=function(t,e){return new n(nt.resolve(t),e)},n.reject=function(t,e){return new n(nt.reject(t),e)},n.race=function(t,e){return new n(nt.race(t),e)};var et=n.prototype;et.bind=function(t){return this.context=t,this},et.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),new n(this.promise.then(t,e),this.context)},et["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),new n(this.promise["catch"](t),this.context)},et["finally"]=function(t){return this.then(function(n){return t.call(this),n},function(n){return t.call(this),nt.reject(n)})};var ot=!1,rt={},it=[],ut=Array.isArray,st=Object.assign||b,ct=document.documentMode,at=document.createElement("a");R.options={url:"",root:null,params:{}},R.transforms=[U,T,w],R.params=function(t){var n=[],e=encodeURIComponent;return n.add=function(t,n){a(n)&&(n=n()),null===n&&(n=""),this.push(e(t)+"="+e(n))},S(n,t),n.join("&").replace(/%20/g,"+")},R.parse=function(t){return ct&&(at.href=t,t=at.href),at.href=t,{href:at.href,protocol:at.protocol?at.protocol.replace(/:$/,""):"",port:at.port,host:at.host,hostname:at.hostname,pathname:"/"===at.pathname.charAt(0)?at.pathname:"/"+at.pathname,search:at.search?at.search.replace(/^\?/,""):"",hash:at.hash?at.hash.replace(/^#/,""):""}};var ft=R.parse(location.href),ht="withCredentials"in new XMLHttpRequest,pt=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},lt=function(){function t(n,e){var o=e.url,r=e.headers,i=e.status,u=e.statusText;pt(this,t),this.url=o,this.body=n,this.headers=r||{},this.status=i||0,this.statusText=u||"",this.ok=i>=200&&i<300}return t.prototype.text=function(){return this.body},t.prototype.blob=function(){return new Blob([this.body])},t.prototype.json=function(){return JSON.parse(this.body)},t}(),dt=function(){function t(n){pt(this,t),this.method="GET",this.body=null,this.params={},this.headers={},st(this,n)}return t.prototype.getUrl=function(){return R(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new lt(t,st(n||{},{url:this.getUrl()}))},t}(),mt={"X-Requested-With":"XMLHttpRequest"},vt={Accept:"application/json, text/plain, */*"},yt={"Content-Type":"application/json;charset=utf-8"};return V.options={},V.headers={put:yt,post:yt,patch:yt,"delete":yt,custom:mt,common:vt},V.interceptors=[D,X,J,L,N,M,H],["get","delete","head","jsonp"].forEach(function(t){V[t]=function(n,e){return this(st(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){V[t]=function(n,e,o){return this(st(o||{},{url:n,method:t,body:e}))}}),_.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},"delete":{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(K),K}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
deleted file mode 100644
index ea15bfac416..00000000000
--- a/vendor/assets/javascripts/vue.full.js
+++ /dev/null
@@ -1,7515 +0,0 @@
-/*!
- * Vue.js v2.0.3
- * (c) 2014-2016 Evan You
- * Released under the MIT License.
- */
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.Vue = factory());
-}(this, (function () { 'use strict';
-
-/* */
-
-/**
- * Convert a value to a string that is actually rendered.
- */
-function _toString (val) {
- return val == null
- ? ''
- : typeof val === 'object'
- ? JSON.stringify(val, null, 2)
- : String(val)
-}
-
-/**
- * Convert a input value to a number for persistence.
- * If the conversion fails, return original string.
- */
-function toNumber (val) {
- var n = parseFloat(val, 10);
- return (n || n === 0) ? n : val
-}
-
-/**
- * Make a map and return a function for checking if a key
- * is in that map.
- */
-function makeMap (
- str,
- expectsLowerCase
-) {
- var map = Object.create(null);
- var list = str.split(',');
- for (var i = 0; i < list.length; i++) {
- map[list[i]] = true;
- }
- return expectsLowerCase
- ? function (val) { return map[val.toLowerCase()]; }
- : function (val) { return map[val]; }
-}
-
-/**
- * Check if a tag is a built-in tag.
- */
-var isBuiltInTag = makeMap('slot,component', true);
-
-/**
- * Remove an item from an array
- */
-function remove$1 (arr, item) {
- if (arr.length) {
- var index = arr.indexOf(item);
- if (index > -1) {
- return arr.splice(index, 1)
- }
- }
-}
-
-/**
- * Check whether the object has the property.
- */
-var hasOwnProperty = Object.prototype.hasOwnProperty;
-function hasOwn (obj, key) {
- return hasOwnProperty.call(obj, key)
-}
-
-/**
- * Check if value is primitive
- */
-function isPrimitive (value) {
- return typeof value === 'string' || typeof value === 'number'
-}
-
-/**
- * Create a cached version of a pure function.
- */
-function cached (fn) {
- var cache = Object.create(null);
- return function cachedFn (str) {
- var hit = cache[str];
- return hit || (cache[str] = fn(str))
- }
-}
-
-/**
- * Camelize a hyphen-delmited string.
- */
-var camelizeRE = /-(\w)/g;
-var camelize = cached(function (str) {
- return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
-});
-
-/**
- * Capitalize a string.
- */
-var capitalize = cached(function (str) {
- return str.charAt(0).toUpperCase() + str.slice(1)
-});
-
-/**
- * Hyphenate a camelCase string.
- */
-var hyphenateRE = /([^-])([A-Z])/g;
-var hyphenate = cached(function (str) {
- return str
- .replace(hyphenateRE, '$1-$2')
- .replace(hyphenateRE, '$1-$2')
- .toLowerCase()
-});
-
-/**
- * Simple bind, faster than native
- */
-function bind$1 (fn, ctx) {
- function boundFn (a) {
- var l = arguments.length;
- return l
- ? l > 1
- ? fn.apply(ctx, arguments)
- : fn.call(ctx, a)
- : fn.call(ctx)
- }
- // record original fn length
- boundFn._length = fn.length;
- return boundFn
-}
-
-/**
- * Convert an Array-like object to a real Array.
- */
-function toArray (list, start) {
- start = start || 0;
- var i = list.length - start;
- var ret = new Array(i);
- while (i--) {
- ret[i] = list[i + start];
- }
- return ret
-}
-
-/**
- * Mix properties into target object.
- */
-function extend (to, _from) {
- for (var key in _from) {
- to[key] = _from[key];
- }
- return to
-}
-
-/**
- * Quick object check - this is primarily used to tell
- * Objects from primitive values when we know the value
- * is a JSON-compliant type.
- */
-function isObject (obj) {
- return obj !== null && typeof obj === 'object'
-}
-
-/**
- * Strict object type check. Only returns true
- * for plain JavaScript objects.
- */
-var toString = Object.prototype.toString;
-var OBJECT_STRING = '[object Object]';
-function isPlainObject (obj) {
- return toString.call(obj) === OBJECT_STRING
-}
-
-/**
- * Merge an Array of Objects into a single Object.
- */
-function toObject (arr) {
- var res = {};
- for (var i = 0; i < arr.length; i++) {
- if (arr[i]) {
- extend(res, arr[i]);
- }
- }
- return res
-}
-
-/**
- * Perform no operation.
- */
-function noop () {}
-
-/**
- * Always return false.
- */
-var no = function () { return false; };
-
-/**
- * Generate a static keys string from compiler modules.
- */
-function genStaticKeys (modules) {
- return modules.reduce(function (keys, m) {
- return keys.concat(m.staticKeys || [])
- }, []).join(',')
-}
-
-/**
- * Check if two values are loosely equal - that is,
- * if they are plain objects, do they have the same shape?
- */
-function looseEqual (a, b) {
- /* eslint-disable eqeqeq */
- return a == b || (
- isObject(a) && isObject(b)
- ? JSON.stringify(a) === JSON.stringify(b)
- : false
- )
- /* eslint-enable eqeqeq */
-}
-
-function looseIndexOf (arr, val) {
- for (var i = 0; i < arr.length; i++) {
- if (looseEqual(arr[i], val)) { return i }
- }
- return -1
-}
-
-/* */
-
-var config = {
- /**
- * Option merge strategies (used in core/util/options)
- */
- optionMergeStrategies: Object.create(null),
-
- /**
- * Whether to suppress warnings.
- */
- silent: false,
-
- /**
- * Whether to enable devtools
- */
- devtools: "development" !== 'production',
-
- /**
- * Error handler for watcher errors
- */
- errorHandler: null,
-
- /**
- * Ignore certain custom elements
- */
- ignoredElements: null,
-
- /**
- * Custom user key aliases for v-on
- */
- keyCodes: Object.create(null),
-
- /**
- * Check if a tag is reserved so that it cannot be registered as a
- * component. This is platform-dependent and may be overwritten.
- */
- isReservedTag: no,
-
- /**
- * Check if a tag is an unknown element.
- * Platform-dependent.
- */
- isUnknownElement: no,
-
- /**
- * Get the namespace of an element
- */
- getTagNamespace: noop,
-
- /**
- * Check if an attribute must be bound using property, e.g. value
- * Platform-dependent.
- */
- mustUseProp: no,
-
- /**
- * List of asset types that a component can own.
- */
- _assetTypes: [
- 'component',
- 'directive',
- 'filter'
- ],
-
- /**
- * List of lifecycle hooks.
- */
- _lifecycleHooks: [
- 'beforeCreate',
- 'created',
- 'beforeMount',
- 'mounted',
- 'beforeUpdate',
- 'updated',
- 'beforeDestroy',
- 'destroyed',
- 'activated',
- 'deactivated'
- ],
-
- /**
- * Max circular updates allowed in a scheduler flush cycle.
- */
- _maxUpdateCount: 100,
-
- /**
- * Server rendering?
- */
- _isServer: "client" === 'server'
-};
-
-/* */
-
-/**
- * Check if a string starts with $ or _
- */
-function isReserved (str) {
- var c = (str + '').charCodeAt(0);
- return c === 0x24 || c === 0x5F
-}
-
-/**
- * Define a property.
- */
-function def (obj, key, val, enumerable) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- });
-}
-
-/**
- * Parse simple path.
- */
-var bailRE = /[^\w\.\$]/;
-function parsePath (path) {
- if (bailRE.test(path)) {
- return
- } else {
- var segments = path.split('.');
- return function (obj) {
- for (var i = 0; i < segments.length; i++) {
- if (!obj) { return }
- obj = obj[segments[i]];
- }
- return obj
- }
- }
-}
-
-/* */
-/* globals MutationObserver */
-
-// can we use __proto__?
-var hasProto = '__proto__' in {};
-
-// Browser environment sniffing
-var inBrowser =
- typeof window !== 'undefined' &&
- Object.prototype.toString.call(window) !== '[object Object]';
-
-var UA = inBrowser && window.navigator.userAgent.toLowerCase();
-var isIE = UA && /msie|trident/.test(UA);
-var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
-var isEdge = UA && UA.indexOf('edge/') > 0;
-var isAndroid = UA && UA.indexOf('android') > 0;
-var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA);
-
-// detect devtools
-var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
-
-/* istanbul ignore next */
-function isNative (Ctor) {
- return /native code/.test(Ctor.toString())
-}
-
-/**
- * Defer a task to execute it asynchronously.
- */
-var nextTick = (function () {
- var callbacks = [];
- var pending = false;
- var timerFunc;
-
- function nextTickHandler () {
- pending = false;
- var copies = callbacks.slice(0);
- callbacks.length = 0;
- for (var i = 0; i < copies.length; i++) {
- copies[i]();
- }
- }
-
- // the nextTick behavior leverages the microtask queue, which can be accessed
- // via either native Promise.then or MutationObserver.
- // MutationObserver has wider support, however it is seriously bugged in
- // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
- // completely stops working after triggering a few times... so, if native
- // Promise is available, we will use it:
- /* istanbul ignore if */
- if (typeof Promise !== 'undefined' && isNative(Promise)) {
- var p = Promise.resolve();
- timerFunc = function () {
- p.then(nextTickHandler);
- // in problematic UIWebViews, Promise.then doesn't completely break, but
- // it can get stuck in a weird state where callbacks are pushed into the
- // microtask queue but the queue isn't being flushed, until the browser
- // needs to do some other work, e.g. handle a timer. Therefore we can
- // "force" the microtask queue to be flushed by adding an empty timer.
- if (isIOS) { setTimeout(noop); }
- };
- } else if (typeof MutationObserver !== 'undefined' && (
- isNative(MutationObserver) ||
- // PhantomJS and iOS 7.x
- MutationObserver.toString() === '[object MutationObserverConstructor]'
- )) {
- // use MutationObserver where native Promise is not available,
- // e.g. PhantomJS IE11, iOS7, Android 4.4
- var counter = 1;
- var observer = new MutationObserver(nextTickHandler);
- var textNode = document.createTextNode(String(counter));
- observer.observe(textNode, {
- characterData: true
- });
- timerFunc = function () {
- counter = (counter + 1) % 2;
- textNode.data = String(counter);
- };
- } else {
- // fallback to setTimeout
- /* istanbul ignore next */
- timerFunc = function () {
- setTimeout(nextTickHandler, 0);
- };
- }
-
- return function queueNextTick (cb, ctx) {
- var func = ctx
- ? function () { cb.call(ctx); }
- : cb;
- callbacks.push(func);
- if (!pending) {
- pending = true;
- timerFunc();
- }
- }
-})();
-
-var _Set;
-/* istanbul ignore if */
-if (typeof Set !== 'undefined' && isNative(Set)) {
- // use native Set when available.
- _Set = Set;
-} else {
- // a non-standard Set polyfill that only works with primitive keys.
- _Set = (function () {
- function Set () {
- this.set = Object.create(null);
- }
- Set.prototype.has = function has (key) {
- return this.set[key] !== undefined
- };
- Set.prototype.add = function add (key) {
- this.set[key] = 1;
- };
- Set.prototype.clear = function clear () {
- this.set = Object.create(null);
- };
-
- return Set;
- }());
-}
-
-/* not type checking this file because flow doesn't play well with Proxy */
-
-var hasProxy;
-var proxyHandlers;
-var initProxy;
-
-{
- var allowedGlobals = makeMap(
- 'Infinity,undefined,NaN,isFinite,isNaN,' +
- 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
- 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
- 'require' // for Webpack/Browserify
- );
-
- hasProxy =
- typeof Proxy !== 'undefined' &&
- Proxy.toString().match(/native code/);
-
- proxyHandlers = {
- has: function has (target, key) {
- var has = key in target;
- var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
- if (!has && !isAllowed) {
- warn(
- "Property or method \"" + key + "\" is not defined on the instance but " +
- "referenced during render. Make sure to declare reactive data " +
- "properties in the data option.",
- target
- );
- }
- return has || !isAllowed
- }
- };
-
- initProxy = function initProxy (vm) {
- if (hasProxy) {
- vm._renderProxy = new Proxy(vm, proxyHandlers);
- } else {
- vm._renderProxy = vm;
- }
- };
-}
-
-/* */
-
-
-var uid$2 = 0;
-
-/**
- * A dep is an observable that can have multiple
- * directives subscribing to it.
- */
-var Dep = function Dep () {
- this.id = uid$2++;
- this.subs = [];
-};
-
-Dep.prototype.addSub = function addSub (sub) {
- this.subs.push(sub);
-};
-
-Dep.prototype.removeSub = function removeSub (sub) {
- remove$1(this.subs, sub);
-};
-
-Dep.prototype.depend = function depend () {
- if (Dep.target) {
- Dep.target.addDep(this);
- }
-};
-
-Dep.prototype.notify = function notify () {
- // stablize the subscriber list first
- var subs = this.subs.slice();
- for (var i = 0, l = subs.length; i < l; i++) {
- subs[i].update();
- }
-};
-
-// the current target watcher being evaluated.
-// this is globally unique because there could be only one
-// watcher being evaluated at any time.
-Dep.target = null;
-var targetStack = [];
-
-function pushTarget (_target) {
- if (Dep.target) { targetStack.push(Dep.target); }
- Dep.target = _target;
-}
-
-function popTarget () {
- Dep.target = targetStack.pop();
-}
-
-/* */
-
-
-var queue = [];
-var has$1 = {};
-var circular = {};
-var waiting = false;
-var flushing = false;
-var index = 0;
-
-/**
- * Reset the scheduler's state.
- */
-function resetSchedulerState () {
- queue.length = 0;
- has$1 = {};
- {
- circular = {};
- }
- waiting = flushing = false;
-}
-
-/**
- * Flush both queues and run the watchers.
- */
-function flushSchedulerQueue () {
- flushing = true;
-
- // Sort queue before flush.
- // This ensures that:
- // 1. Components are updated from parent to child. (because parent is always
- // created before the child)
- // 2. A component's user watchers are run before its render watcher (because
- // user watchers are created before the render watcher)
- // 3. If a component is destroyed during a parent component's watcher run,
- // its watchers can be skipped.
- queue.sort(function (a, b) { return a.id - b.id; });
-
- // do not cache length because more watchers might be pushed
- // as we run existing watchers
- for (index = 0; index < queue.length; index++) {
- var watcher = queue[index];
- var id = watcher.id;
- has$1[id] = null;
- watcher.run();
- // in dev build, check and stop circular updates.
- if ("development" !== 'production' && has$1[id] != null) {
- circular[id] = (circular[id] || 0) + 1;
- if (circular[id] > config._maxUpdateCount) {
- warn(
- 'You may have an infinite update loop ' + (
- watcher.user
- ? ("in watcher with expression \"" + (watcher.expression) + "\"")
- : "in a component render function."
- ),
- watcher.vm
- );
- break
- }
- }
- }
-
- // devtool hook
- /* istanbul ignore if */
- if (devtools && config.devtools) {
- devtools.emit('flush');
- }
-
- resetSchedulerState();
-}
-
-/**
- * Push a watcher into the watcher queue.
- * Jobs with duplicate IDs will be skipped unless it's
- * pushed when the queue is being flushed.
- */
-function queueWatcher (watcher) {
- var id = watcher.id;
- if (has$1[id] == null) {
- has$1[id] = true;
- if (!flushing) {
- queue.push(watcher);
- } else {
- // if already flushing, splice the watcher based on its id
- // if already past its id, it will be run next immediately.
- var i = queue.length - 1;
- while (i >= 0 && queue[i].id > watcher.id) {
- i--;
- }
- queue.splice(Math.max(i, index) + 1, 0, watcher);
- }
- // queue the flush
- if (!waiting) {
- waiting = true;
- nextTick(flushSchedulerQueue);
- }
- }
-}
-
-/* */
-
-var uid$1 = 0;
-
-/**
- * A watcher parses an expression, collects dependencies,
- * and fires callback when the expression value changes.
- * This is used for both the $watch() api and directives.
- */
-var Watcher = function Watcher (
- vm,
- expOrFn,
- cb,
- options
-) {
- if ( options === void 0 ) options = {};
-
- this.vm = vm;
- vm._watchers.push(this);
- // options
- this.deep = !!options.deep;
- this.user = !!options.user;
- this.lazy = !!options.lazy;
- this.sync = !!options.sync;
- this.expression = expOrFn.toString();
- this.cb = cb;
- this.id = ++uid$1; // uid for batching
- this.active = true;
- this.dirty = this.lazy; // for lazy watchers
- this.deps = [];
- this.newDeps = [];
- this.depIds = new _Set();
- this.newDepIds = new _Set();
- // parse expression for getter
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn;
- } else {
- this.getter = parsePath(expOrFn);
- if (!this.getter) {
- this.getter = function () {};
- "development" !== 'production' && warn(
- "Failed watching path: \"" + expOrFn + "\" " +
- 'Watcher only accepts simple dot-delimited paths. ' +
- 'For full control, use a function instead.',
- vm
- );
- }
- }
- this.value = this.lazy
- ? undefined
- : this.get();
-};
-
-/**
- * Evaluate the getter, and re-collect dependencies.
- */
-Watcher.prototype.get = function get () {
- pushTarget(this);
- var value = this.getter.call(this.vm, this.vm);
- // "touch" every property so they are all tracked as
- // dependencies for deep watching
- if (this.deep) {
- traverse(value);
- }
- popTarget();
- this.cleanupDeps();
- return value
-};
-
-/**
- * Add a dependency to this directive.
- */
-Watcher.prototype.addDep = function addDep (dep) {
- var id = dep.id;
- if (!this.newDepIds.has(id)) {
- this.newDepIds.add(id);
- this.newDeps.push(dep);
- if (!this.depIds.has(id)) {
- dep.addSub(this);
- }
- }
-};
-
-/**
- * Clean up for dependency collection.
- */
-Watcher.prototype.cleanupDeps = function cleanupDeps () {
- var this$1 = this;
-
- var i = this.deps.length;
- while (i--) {
- var dep = this$1.deps[i];
- if (!this$1.newDepIds.has(dep.id)) {
- dep.removeSub(this$1);
- }
- }
- var tmp = this.depIds;
- this.depIds = this.newDepIds;
- this.newDepIds = tmp;
- this.newDepIds.clear();
- tmp = this.deps;
- this.deps = this.newDeps;
- this.newDeps = tmp;
- this.newDeps.length = 0;
-};
-
-/**
- * Subscriber interface.
- * Will be called when a dependency changes.
- */
-Watcher.prototype.update = function update () {
- /* istanbul ignore else */
- if (this.lazy) {
- this.dirty = true;
- } else if (this.sync) {
- this.run();
- } else {
- queueWatcher(this);
- }
-};
-
-/**
- * Scheduler job interface.
- * Will be called by the scheduler.
- */
-Watcher.prototype.run = function run () {
- if (this.active) {
- var value = this.get();
- if (
- value !== this.value ||
- // Deep watchers and watchers on Object/Arrays should fire even
- // when the value is the same, because the value may
- // have mutated.
- isObject(value) ||
- this.deep
- ) {
- // set new value
- var oldValue = this.value;
- this.value = value;
- if (this.user) {
- try {
- this.cb.call(this.vm, value, oldValue);
- } catch (e) {
- "development" !== 'production' && warn(
- ("Error in watcher \"" + (this.expression) + "\""),
- this.vm
- );
- /* istanbul ignore else */
- if (config.errorHandler) {
- config.errorHandler.call(null, e, this.vm);
- } else {
- throw e
- }
- }
- } else {
- this.cb.call(this.vm, value, oldValue);
- }
- }
- }
-};
-
-/**
- * Evaluate the value of the watcher.
- * This only gets called for lazy watchers.
- */
-Watcher.prototype.evaluate = function evaluate () {
- this.value = this.get();
- this.dirty = false;
-};
-
-/**
- * Depend on all deps collected by this watcher.
- */
-Watcher.prototype.depend = function depend () {
- var this$1 = this;
-
- var i = this.deps.length;
- while (i--) {
- this$1.deps[i].depend();
- }
-};
-
-/**
- * Remove self from all dependencies' subcriber list.
- */
-Watcher.prototype.teardown = function teardown () {
- var this$1 = this;
-
- if (this.active) {
- // remove self from vm's watcher list
- // this is a somewhat expensive operation so we skip it
- // if the vm is being destroyed or is performing a v-for
- // re-render (the watcher list is then filtered by v-for).
- if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
- remove$1(this.vm._watchers, this);
- }
- var i = this.deps.length;
- while (i--) {
- this$1.deps[i].removeSub(this$1);
- }
- this.active = false;
- }
-};
-
-/**
- * Recursively traverse an object to evoke all converted
- * getters, so that every nested property inside the object
- * is collected as a "deep" dependency.
- */
-var seenObjects = new _Set();
-function traverse (val, seen) {
- var i, keys;
- if (!seen) {
- seen = seenObjects;
- seen.clear();
- }
- var isA = Array.isArray(val);
- var isO = isObject(val);
- if ((isA || isO) && Object.isExtensible(val)) {
- if (val.__ob__) {
- var depId = val.__ob__.dep.id;
- if (seen.has(depId)) {
- return
- } else {
- seen.add(depId);
- }
- }
- if (isA) {
- i = val.length;
- while (i--) { traverse(val[i], seen); }
- } else if (isO) {
- keys = Object.keys(val);
- i = keys.length;
- while (i--) { traverse(val[keys[i]], seen); }
- }
- }
-}
-
-/*
- * not type checking this file because flow doesn't play well with
- * dynamically accessing methods on Array prototype
- */
-
-var arrayProto = Array.prototype;
-var arrayMethods = Object.create(arrayProto);[
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
-]
-.forEach(function (method) {
- // cache original method
- var original = arrayProto[method];
- def(arrayMethods, method, function mutator () {
- var arguments$1 = arguments;
-
- // avoid leaking arguments:
- // http://jsperf.com/closure-with-arguments
- var i = arguments.length;
- var args = new Array(i);
- while (i--) {
- args[i] = arguments$1[i];
- }
- var result = original.apply(this, args);
- var ob = this.__ob__;
- var inserted;
- switch (method) {
- case 'push':
- inserted = args;
- break
- case 'unshift':
- inserted = args;
- break
- case 'splice':
- inserted = args.slice(2);
- break
- }
- if (inserted) { ob.observeArray(inserted); }
- // notify change
- ob.dep.notify();
- return result
- });
-});
-
-/* */
-
-var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
-
-/**
- * By default, when a reactive property is set, the new value is
- * also converted to become reactive. However when passing down props,
- * we don't want to force conversion because the value may be a nested value
- * under a frozen data structure. Converting it would defeat the optimization.
- */
-var observerState = {
- shouldConvert: true,
- isSettingProps: false
-};
-
-/**
- * Observer class that are attached to each observed
- * object. Once attached, the observer converts target
- * object's property keys into getter/setters that
- * collect dependencies and dispatches updates.
- */
-var Observer = function Observer (value) {
- this.value = value;
- this.dep = new Dep();
- this.vmCount = 0;
- def(value, '__ob__', this);
- if (Array.isArray(value)) {
- var augment = hasProto
- ? protoAugment
- : copyAugment;
- augment(value, arrayMethods, arrayKeys);
- this.observeArray(value);
- } else {
- this.walk(value);
- }
-};
-
-/**
- * Walk through each property and convert them into
- * getter/setters. This method should only be called when
- * value type is Object.
- */
-Observer.prototype.walk = function walk (obj) {
- var keys = Object.keys(obj);
- for (var i = 0; i < keys.length; i++) {
- defineReactive$$1(obj, keys[i], obj[keys[i]]);
- }
-};
-
-/**
- * Observe a list of Array items.
- */
-Observer.prototype.observeArray = function observeArray (items) {
- for (var i = 0, l = items.length; i < l; i++) {
- observe(items[i]);
- }
-};
-
-// helpers
-
-/**
- * Augment an target Object or Array by intercepting
- * the prototype chain using __proto__
- */
-function protoAugment (target, src) {
- /* eslint-disable no-proto */
- target.__proto__ = src;
- /* eslint-enable no-proto */
-}
-
-/**
- * Augment an target Object or Array by defining
- * hidden properties.
- *
- * istanbul ignore next
- */
-function copyAugment (target, src, keys) {
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- def(target, key, src[key]);
- }
-}
-
-/**
- * Attempt to create an observer instance for a value,
- * returns the new observer if successfully observed,
- * or the existing observer if the value already has one.
- */
-function observe (value) {
- if (!isObject(value)) {
- return
- }
- var ob;
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__;
- } else if (
- observerState.shouldConvert &&
- !config._isServer &&
- (Array.isArray(value) || isPlainObject(value)) &&
- Object.isExtensible(value) &&
- !value._isVue
- ) {
- ob = new Observer(value);
- }
- return ob
-}
-
-/**
- * Define a reactive property on an Object.
- */
-function defineReactive$$1 (
- obj,
- key,
- val,
- customSetter
-) {
- var dep = new Dep();
-
- var property = Object.getOwnPropertyDescriptor(obj, key);
- if (property && property.configurable === false) {
- return
- }
-
- // cater for pre-defined getter/setters
- var getter = property && property.get;
- var setter = property && property.set;
-
- var childOb = observe(val);
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- var value = getter ? getter.call(obj) : val;
- if (Dep.target) {
- dep.depend();
- if (childOb) {
- childOb.dep.depend();
- }
- if (Array.isArray(value)) {
- dependArray(value);
- }
- }
- return value
- },
- set: function reactiveSetter (newVal) {
- var value = getter ? getter.call(obj) : val;
- if (newVal === value) {
- return
- }
- if ("development" !== 'production' && customSetter) {
- customSetter();
- }
- if (setter) {
- setter.call(obj, newVal);
- } else {
- val = newVal;
- }
- childOb = observe(newVal);
- dep.notify();
- }
- });
-}
-
-/**
- * Set a property on an object. Adds the new property and
- * triggers change notification if the property doesn't
- * already exist.
- */
-function set (obj, key, val) {
- if (Array.isArray(obj)) {
- obj.splice(key, 1, val);
- return val
- }
- if (hasOwn(obj, key)) {
- obj[key] = val;
- return
- }
- var ob = obj.__ob__;
- if (obj._isVue || (ob && ob.vmCount)) {
- "development" !== 'production' && warn(
- 'Avoid adding reactive properties to a Vue instance or its root $data ' +
- 'at runtime - declare it upfront in the data option.'
- );
- return
- }
- if (!ob) {
- obj[key] = val;
- return
- }
- defineReactive$$1(ob.value, key, val);
- ob.dep.notify();
- return val
-}
-
-/**
- * Delete a property and trigger change if necessary.
- */
-function del (obj, key) {
- var ob = obj.__ob__;
- if (obj._isVue || (ob && ob.vmCount)) {
- "development" !== 'production' && warn(
- 'Avoid deleting properties on a Vue instance or its root $data ' +
- '- just set it to null.'
- );
- return
- }
- if (!hasOwn(obj, key)) {
- return
- }
- delete obj[key];
- if (!ob) {
- return
- }
- ob.dep.notify();
-}
-
-/**
- * Collect dependencies on array elements when the array is touched, since
- * we cannot intercept array element access like property getters.
- */
-function dependArray (value) {
- for (var e = void 0, i = 0, l = value.length; i < l; i++) {
- e = value[i];
- e && e.__ob__ && e.__ob__.dep.depend();
- if (Array.isArray(e)) {
- dependArray(e);
- }
- }
-}
-
-/* */
-
-function initState (vm) {
- vm._watchers = [];
- initProps(vm);
- initData(vm);
- initComputed(vm);
- initMethods(vm);
- initWatch(vm);
-}
-
-function initProps (vm) {
- var props = vm.$options.props;
- if (props) {
- var propsData = vm.$options.propsData || {};
- var keys = vm.$options._propKeys = Object.keys(props);
- var isRoot = !vm.$parent;
- // root instance props should be converted
- observerState.shouldConvert = isRoot;
- var loop = function ( i ) {
- var key = keys[i];
- /* istanbul ignore else */
- {
- defineReactive$$1(vm, key, validateProp(key, props, propsData, vm), function () {
- if (vm.$parent && !observerState.isSettingProps) {
- warn(
- "Avoid mutating a prop directly since the value will be " +
- "overwritten whenever the parent component re-renders. " +
- "Instead, use a data or computed property based on the prop's " +
- "value. Prop being mutated: \"" + key + "\"",
- vm
- );
- }
- });
- }
- };
-
- for (var i = 0; i < keys.length; i++) loop( i );
- observerState.shouldConvert = true;
- }
-}
-
-function initData (vm) {
- var data = vm.$options.data;
- data = vm._data = typeof data === 'function'
- ? data.call(vm)
- : data || {};
- if (!isPlainObject(data)) {
- data = {};
- "development" !== 'production' && warn(
- 'data functions should return an object.',
- vm
- );
- }
- // proxy data on instance
- var keys = Object.keys(data);
- var props = vm.$options.props;
- var i = keys.length;
- while (i--) {
- if (props && hasOwn(props, keys[i])) {
- "development" !== 'production' && warn(
- "The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
- "Use prop default value instead.",
- vm
- );
- } else {
- proxy(vm, keys[i]);
- }
- }
- // observe data
- observe(data);
- data.__ob__ && data.__ob__.vmCount++;
-}
-
-var computedSharedDefinition = {
- enumerable: true,
- configurable: true,
- get: noop,
- set: noop
-};
-
-function initComputed (vm) {
- var computed = vm.$options.computed;
- if (computed) {
- for (var key in computed) {
- var userDef = computed[key];
- if (typeof userDef === 'function') {
- computedSharedDefinition.get = makeComputedGetter(userDef, vm);
- computedSharedDefinition.set = noop;
- } else {
- computedSharedDefinition.get = userDef.get
- ? userDef.cache !== false
- ? makeComputedGetter(userDef.get, vm)
- : bind$1(userDef.get, vm)
- : noop;
- computedSharedDefinition.set = userDef.set
- ? bind$1(userDef.set, vm)
- : noop;
- }
- Object.defineProperty(vm, key, computedSharedDefinition);
- }
- }
-}
-
-function makeComputedGetter (getter, owner) {
- var watcher = new Watcher(owner, getter, noop, {
- lazy: true
- });
- return function computedGetter () {
- if (watcher.dirty) {
- watcher.evaluate();
- }
- if (Dep.target) {
- watcher.depend();
- }
- return watcher.value
- }
-}
-
-function initMethods (vm) {
- var methods = vm.$options.methods;
- if (methods) {
- for (var key in methods) {
- vm[key] = methods[key] == null ? noop : bind$1(methods[key], vm);
- if ("development" !== 'production' && methods[key] == null) {
- warn(
- "method \"" + key + "\" has an undefined value in the component definition. " +
- "Did you reference the function correctly?",
- vm
- );
- }
- }
- }
-}
-
-function initWatch (vm) {
- var watch = vm.$options.watch;
- if (watch) {
- for (var key in watch) {
- var handler = watch[key];
- if (Array.isArray(handler)) {
- for (var i = 0; i < handler.length; i++) {
- createWatcher(vm, key, handler[i]);
- }
- } else {
- createWatcher(vm, key, handler);
- }
- }
- }
-}
-
-function createWatcher (vm, key, handler) {
- var options;
- if (isPlainObject(handler)) {
- options = handler;
- handler = handler.handler;
- }
- if (typeof handler === 'string') {
- handler = vm[handler];
- }
- vm.$watch(key, handler, options);
-}
-
-function stateMixin (Vue) {
- // flow somehow has problems with directly declared definition object
- // when using Object.defineProperty, so we have to procedurally build up
- // the object here.
- var dataDef = {};
- dataDef.get = function () {
- return this._data
- };
- {
- dataDef.set = function (newData) {
- warn(
- 'Avoid replacing instance root $data. ' +
- 'Use nested data properties instead.',
- this
- );
- };
- }
- Object.defineProperty(Vue.prototype, '$data', dataDef);
-
- Vue.prototype.$set = set;
- Vue.prototype.$delete = del;
-
- Vue.prototype.$watch = function (
- expOrFn,
- cb,
- options
- ) {
- var vm = this;
- options = options || {};
- options.user = true;
- var watcher = new Watcher(vm, expOrFn, cb, options);
- if (options.immediate) {
- cb.call(vm, watcher.value);
- }
- return function unwatchFn () {
- watcher.teardown();
- }
- };
-}
-
-function proxy (vm, key) {
- if (!isReserved(key)) {
- Object.defineProperty(vm, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter () {
- return vm._data[key]
- },
- set: function proxySetter (val) {
- vm._data[key] = val;
- }
- });
- }
-}
-
-/* */
-
-var VNode = function VNode (
- tag,
- data,
- children,
- text,
- elm,
- ns,
- context,
- componentOptions
-) {
- this.tag = tag;
- this.data = data;
- this.children = children;
- this.text = text;
- this.elm = elm;
- this.ns = ns;
- this.context = context;
- this.functionalContext = undefined;
- this.key = data && data.key;
- this.componentOptions = componentOptions;
- this.child = undefined;
- this.parent = undefined;
- this.raw = false;
- this.isStatic = false;
- this.isRootInsert = true;
- this.isComment = false;
- this.isCloned = false;
-};
-
-var emptyVNode = function () {
- var node = new VNode();
- node.text = '';
- node.isComment = true;
- return node
-};
-
-// optimized shallow clone
-// used for static nodes and slot nodes because they may be reused across
-// multiple renders, cloning them avoids errors when DOM manipulations rely
-// on their elm reference.
-function cloneVNode (vnode) {
- var cloned = new VNode(
- vnode.tag,
- vnode.data,
- vnode.children,
- vnode.text,
- vnode.elm,
- vnode.ns,
- vnode.context,
- vnode.componentOptions
- );
- cloned.isStatic = vnode.isStatic;
- cloned.key = vnode.key;
- cloned.isCloned = true;
- return cloned
-}
-
-function cloneVNodes (vnodes) {
- var res = new Array(vnodes.length);
- for (var i = 0; i < vnodes.length; i++) {
- res[i] = cloneVNode(vnodes[i]);
- }
- return res
-}
-
-/* */
-
-function mergeVNodeHook (def, hookKey, hook, key) {
- key = key + hookKey;
- var injectedHash = def.__injected || (def.__injected = {});
- if (!injectedHash[key]) {
- injectedHash[key] = true;
- var oldHook = def[hookKey];
- if (oldHook) {
- def[hookKey] = function () {
- oldHook.apply(this, arguments);
- hook.apply(this, arguments);
- };
- } else {
- def[hookKey] = hook;
- }
- }
-}
-
-/* */
-
-function updateListeners (
- on,
- oldOn,
- add,
- remove$$1,
- vm
-) {
- var name, cur, old, fn, event, capture;
- for (name in on) {
- cur = on[name];
- old = oldOn[name];
- if (!cur) {
- "development" !== 'production' && warn(
- "Invalid handler for event \"" + name + "\": got " + String(cur),
- vm
- );
- } else if (!old) {
- capture = name.charAt(0) === '!';
- event = capture ? name.slice(1) : name;
- if (Array.isArray(cur)) {
- add(event, (cur.invoker = arrInvoker(cur)), capture);
- } else {
- if (!cur.invoker) {
- fn = cur;
- cur = on[name] = {};
- cur.fn = fn;
- cur.invoker = fnInvoker(cur);
- }
- add(event, cur.invoker, capture);
- }
- } else if (cur !== old) {
- if (Array.isArray(old)) {
- old.length = cur.length;
- for (var i = 0; i < old.length; i++) { old[i] = cur[i]; }
- on[name] = old;
- } else {
- old.fn = cur;
- on[name] = old;
- }
- }
- }
- for (name in oldOn) {
- if (!on[name]) {
- event = name.charAt(0) === '!' ? name.slice(1) : name;
- remove$$1(event, oldOn[name].invoker);
- }
- }
-}
-
-function arrInvoker (arr) {
- return function (ev) {
- var arguments$1 = arguments;
-
- var single = arguments.length === 1;
- for (var i = 0; i < arr.length; i++) {
- single ? arr[i](ev) : arr[i].apply(null, arguments$1);
- }
- }
-}
-
-function fnInvoker (o) {
- return function (ev) {
- var single = arguments.length === 1;
- single ? o.fn(ev) : o.fn.apply(null, arguments);
- }
-}
-
-/* */
-
-function normalizeChildren (
- children,
- ns,
- nestedIndex
-) {
- if (isPrimitive(children)) {
- return [createTextVNode(children)]
- }
- if (Array.isArray(children)) {
- var res = [];
- for (var i = 0, l = children.length; i < l; i++) {
- var c = children[i];
- var last = res[res.length - 1];
- // nested
- if (Array.isArray(c)) {
- res.push.apply(res, normalizeChildren(c, ns, ((nestedIndex || '') + "_" + i)));
- } else if (isPrimitive(c)) {
- if (last && last.text) {
- last.text += String(c);
- } else if (c !== '') {
- // convert primitive to vnode
- res.push(createTextVNode(c));
- }
- } else if (c instanceof VNode) {
- if (c.text && last && last.text) {
- last.text += c.text;
- } else {
- // inherit parent namespace
- if (ns) {
- applyNS(c, ns);
- }
- // default key for nested array children (likely generated by v-for)
- if (c.tag && c.key == null && nestedIndex != null) {
- c.key = "__vlist" + nestedIndex + "_" + i + "__";
- }
- res.push(c);
- }
- }
- }
- return res
- }
-}
-
-function createTextVNode (val) {
- return new VNode(undefined, undefined, undefined, String(val))
-}
-
-function applyNS (vnode, ns) {
- if (vnode.tag && !vnode.ns) {
- vnode.ns = ns;
- if (vnode.children) {
- for (var i = 0, l = vnode.children.length; i < l; i++) {
- applyNS(vnode.children[i], ns);
- }
- }
- }
-}
-
-/* */
-
-function getFirstComponentChild (children) {
- return children && children.filter(function (c) { return c && c.componentOptions; })[0]
-}
-
-/* */
-
-var activeInstance = null;
-
-function initLifecycle (vm) {
- var options = vm.$options;
-
- // locate first non-abstract parent
- var parent = options.parent;
- if (parent && !options.abstract) {
- while (parent.$options.abstract && parent.$parent) {
- parent = parent.$parent;
- }
- parent.$children.push(vm);
- }
-
- vm.$parent = parent;
- vm.$root = parent ? parent.$root : vm;
-
- vm.$children = [];
- vm.$refs = {};
-
- vm._watcher = null;
- vm._inactive = false;
- vm._isMounted = false;
- vm._isDestroyed = false;
- vm._isBeingDestroyed = false;
-}
-
-function lifecycleMixin (Vue) {
- Vue.prototype._mount = function (
- el,
- hydrating
- ) {
- var vm = this;
- vm.$el = el;
- if (!vm.$options.render) {
- vm.$options.render = emptyVNode;
- {
- /* istanbul ignore if */
- if (vm.$options.template) {
- warn(
- 'You are using the runtime-only build of Vue where the template ' +
- 'option is not available. Either pre-compile the templates into ' +
- 'render functions, or use the compiler-included build.',
- vm
- );
- } else {
- warn(
- 'Failed to mount component: template or render function not defined.',
- vm
- );
- }
- }
- }
- callHook(vm, 'beforeMount');
- vm._watcher = new Watcher(vm, function () {
- vm._update(vm._render(), hydrating);
- }, noop);
- hydrating = false;
- // manually mounted instance, call mounted on self
- // mounted is called for render-created child components in its inserted hook
- if (vm.$vnode == null) {
- vm._isMounted = true;
- callHook(vm, 'mounted');
- }
- return vm
- };
-
- Vue.prototype._update = function (vnode, hydrating) {
- var vm = this;
- if (vm._isMounted) {
- callHook(vm, 'beforeUpdate');
- }
- var prevEl = vm.$el;
- var prevActiveInstance = activeInstance;
- activeInstance = vm;
- var prevVnode = vm._vnode;
- vm._vnode = vnode;
- if (!prevVnode) {
- // Vue.prototype.__patch__ is injected in entry points
- // based on the rendering backend used.
- vm.$el = vm.__patch__(vm.$el, vnode, hydrating);
- } else {
- vm.$el = vm.__patch__(prevVnode, vnode);
- }
- activeInstance = prevActiveInstance;
- // update __vue__ reference
- if (prevEl) {
- prevEl.__vue__ = null;
- }
- if (vm.$el) {
- vm.$el.__vue__ = vm;
- }
- // if parent is an HOC, update its $el as well
- if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
- vm.$parent.$el = vm.$el;
- }
- if (vm._isMounted) {
- callHook(vm, 'updated');
- }
- };
-
- Vue.prototype._updateFromParent = function (
- propsData,
- listeners,
- parentVnode,
- renderChildren
- ) {
- var vm = this;
- var hasChildren = !!(vm.$options._renderChildren || renderChildren);
- vm.$options._parentVnode = parentVnode;
- vm.$options._renderChildren = renderChildren;
- // update props
- if (propsData && vm.$options.props) {
- observerState.shouldConvert = false;
- {
- observerState.isSettingProps = true;
- }
- var propKeys = vm.$options._propKeys || [];
- for (var i = 0; i < propKeys.length; i++) {
- var key = propKeys[i];
- vm[key] = validateProp(key, vm.$options.props, propsData, vm);
- }
- observerState.shouldConvert = true;
- {
- observerState.isSettingProps = false;
- }
- }
- // update listeners
- if (listeners) {
- var oldListeners = vm.$options._parentListeners;
- vm.$options._parentListeners = listeners;
- vm._updateListeners(listeners, oldListeners);
- }
- // resolve slots + force update if has children
- if (hasChildren) {
- vm.$slots = resolveSlots(renderChildren, vm._renderContext);
- vm.$forceUpdate();
- }
- };
-
- Vue.prototype.$forceUpdate = function () {
- var vm = this;
- if (vm._watcher) {
- vm._watcher.update();
- }
- };
-
- Vue.prototype.$destroy = function () {
- var vm = this;
- if (vm._isBeingDestroyed) {
- return
- }
- callHook(vm, 'beforeDestroy');
- vm._isBeingDestroyed = true;
- // remove self from parent
- var parent = vm.$parent;
- if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
- remove$1(parent.$children, vm);
- }
- // teardown watchers
- if (vm._watcher) {
- vm._watcher.teardown();
- }
- var i = vm._watchers.length;
- while (i--) {
- vm._watchers[i].teardown();
- }
- // remove reference from data ob
- // frozen object may not have observer.
- if (vm._data.__ob__) {
- vm._data.__ob__.vmCount--;
- }
- // call the last hook...
- vm._isDestroyed = true;
- callHook(vm, 'destroyed');
- // turn off all instance listeners.
- vm.$off();
- // remove __vue__ reference
- if (vm.$el) {
- vm.$el.__vue__ = null;
- }
- // invoke destroy hooks on current rendered tree
- vm.__patch__(vm._vnode, null);
- };
-}
-
-function callHook (vm, hook) {
- var handlers = vm.$options[hook];
- if (handlers) {
- for (var i = 0, j = handlers.length; i < j; i++) {
- handlers[i].call(vm);
- }
- }
- vm.$emit('hook:' + hook);
-}
-
-/* */
-
-var hooks = { init: init, prepatch: prepatch, insert: insert, destroy: destroy$1 };
-var hooksToMerge = Object.keys(hooks);
-
-function createComponent (
- Ctor,
- data,
- context,
- children,
- tag
-) {
- if (!Ctor) {
- return
- }
-
- if (isObject(Ctor)) {
- Ctor = Vue$3.extend(Ctor);
- }
-
- if (typeof Ctor !== 'function') {
- {
- warn(("Invalid Component definition: " + (String(Ctor))), context);
- }
- return
- }
-
- // async component
- if (!Ctor.cid) {
- if (Ctor.resolved) {
- Ctor = Ctor.resolved;
- } else {
- Ctor = resolveAsyncComponent(Ctor, function () {
- // it's ok to queue this on every render because
- // $forceUpdate is buffered by the scheduler.
- context.$forceUpdate();
- });
- if (!Ctor) {
- // return nothing if this is indeed an async component
- // wait for the callback to trigger parent update.
- return
- }
- }
- }
-
- data = data || {};
-
- // extract props
- var propsData = extractProps(data, Ctor);
-
- // functional component
- if (Ctor.options.functional) {
- return createFunctionalComponent(Ctor, propsData, data, context, children)
- }
-
- // extract listeners, since these needs to be treated as
- // child component listeners instead of DOM listeners
- var listeners = data.on;
- // replace with listeners with .native modifier
- data.on = data.nativeOn;
-
- if (Ctor.options.abstract) {
- // abstract components do not keep anything
- // other than props & listeners
- data = {};
- }
-
- // merge component management hooks onto the placeholder node
- mergeHooks(data);
-
- // return a placeholder vnode
- var name = Ctor.options.name || tag;
- var vnode = new VNode(
- ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
- data, undefined, undefined, undefined, undefined, context,
- { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
- );
- return vnode
-}
-
-function createFunctionalComponent (
- Ctor,
- propsData,
- data,
- context,
- children
-) {
- var props = {};
- var propOptions = Ctor.options.props;
- if (propOptions) {
- for (var key in propOptions) {
- props[key] = validateProp(key, propOptions, propsData);
- }
- }
- var vnode = Ctor.options.render.call(
- null,
- // ensure the createElement function in functional components
- // gets a unique context - this is necessary for correct named slot check
- bind$1(createElement, { _self: Object.create(context) }),
- {
- props: props,
- data: data,
- parent: context,
- children: normalizeChildren(children),
- slots: function () { return resolveSlots(children, context); }
- }
- );
- if (vnode instanceof VNode) {
- vnode.functionalContext = context;
- if (data.slot) {
- (vnode.data || (vnode.data = {})).slot = data.slot;
- }
- }
- return vnode
-}
-
-function createComponentInstanceForVnode (
- vnode, // we know it's MountedComponentVNode but flow doesn't
- parent // activeInstance in lifecycle state
-) {
- var vnodeComponentOptions = vnode.componentOptions;
- var options = {
- _isComponent: true,
- parent: parent,
- propsData: vnodeComponentOptions.propsData,
- _componentTag: vnodeComponentOptions.tag,
- _parentVnode: vnode,
- _parentListeners: vnodeComponentOptions.listeners,
- _renderChildren: vnodeComponentOptions.children
- };
- // check inline-template render functions
- var inlineTemplate = vnode.data.inlineTemplate;
- if (inlineTemplate) {
- options.render = inlineTemplate.render;
- options.staticRenderFns = inlineTemplate.staticRenderFns;
- }
- return new vnodeComponentOptions.Ctor(options)
-}
-
-function init (vnode, hydrating) {
- if (!vnode.child || vnode.child._isDestroyed) {
- var child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance);
- child.$mount(hydrating ? vnode.elm : undefined, hydrating);
- }
-}
-
-function prepatch (
- oldVnode,
- vnode
-) {
- var options = vnode.componentOptions;
- var child = vnode.child = oldVnode.child;
- child._updateFromParent(
- options.propsData, // updated props
- options.listeners, // updated listeners
- vnode, // new parent vnode
- options.children // new children
- );
-}
-
-function insert (vnode) {
- if (!vnode.child._isMounted) {
- vnode.child._isMounted = true;
- callHook(vnode.child, 'mounted');
- }
- if (vnode.data.keepAlive) {
- vnode.child._inactive = false;
- callHook(vnode.child, 'activated');
- }
-}
-
-function destroy$1 (vnode) {
- if (!vnode.child._isDestroyed) {
- if (!vnode.data.keepAlive) {
- vnode.child.$destroy();
- } else {
- vnode.child._inactive = true;
- callHook(vnode.child, 'deactivated');
- }
- }
-}
-
-function resolveAsyncComponent (
- factory,
- cb
-) {
- if (factory.requested) {
- // pool callbacks
- factory.pendingCallbacks.push(cb);
- } else {
- factory.requested = true;
- var cbs = factory.pendingCallbacks = [cb];
- var sync = true;
-
- var resolve = function (res) {
- if (isObject(res)) {
- res = Vue$3.extend(res);
- }
- // cache resolved
- factory.resolved = res;
- // invoke callbacks only if this is not a synchronous resolve
- // (async resolves are shimmed as synchronous during SSR)
- if (!sync) {
- for (var i = 0, l = cbs.length; i < l; i++) {
- cbs[i](res);
- }
- }
- };
-
- var reject = function (reason) {
- "development" !== 'production' && warn(
- "Failed to resolve async component: " + (String(factory)) +
- (reason ? ("\nReason: " + reason) : '')
- );
- };
-
- var res = factory(resolve, reject);
-
- // handle promise
- if (res && typeof res.then === 'function' && !factory.resolved) {
- res.then(resolve, reject);
- }
-
- sync = false;
- // return in case resolved synchronously
- return factory.resolved
- }
-}
-
-function extractProps (data, Ctor) {
- // we are only extrating raw values here.
- // validation and default values are handled in the child
- // component itself.
- var propOptions = Ctor.options.props;
- if (!propOptions) {
- return
- }
- var res = {};
- var attrs = data.attrs;
- var props = data.props;
- var domProps = data.domProps;
- if (attrs || props || domProps) {
- for (var key in propOptions) {
- var altKey = hyphenate(key);
- checkProp(res, props, key, altKey, true) ||
- checkProp(res, attrs, key, altKey) ||
- checkProp(res, domProps, key, altKey);
- }
- }
- return res
-}
-
-function checkProp (
- res,
- hash,
- key,
- altKey,
- preserve
-) {
- if (hash) {
- if (hasOwn(hash, key)) {
- res[key] = hash[key];
- if (!preserve) {
- delete hash[key];
- }
- return true
- } else if (hasOwn(hash, altKey)) {
- res[key] = hash[altKey];
- if (!preserve) {
- delete hash[altKey];
- }
- return true
- }
- }
- return false
-}
-
-function mergeHooks (data) {
- if (!data.hook) {
- data.hook = {};
- }
- for (var i = 0; i < hooksToMerge.length; i++) {
- var key = hooksToMerge[i];
- var fromParent = data.hook[key];
- var ours = hooks[key];
- data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours;
- }
-}
-
-function mergeHook$1 (a, b) {
- // since all hooks have at most two args, use fixed args
- // to avoid having to use fn.apply().
- return function (_, __) {
- a(_, __);
- b(_, __);
- }
-}
-
-/* */
-
-// wrapper function for providing a more flexible interface
-// without getting yelled at by flow
-function createElement (
- tag,
- data,
- children
-) {
- if (data && (Array.isArray(data) || typeof data !== 'object')) {
- children = data;
- data = undefined;
- }
- // make sure to use real instance instead of proxy as context
- return _createElement(this._self, tag, data, children)
-}
-
-function _createElement (
- context,
- tag,
- data,
- children
-) {
- if (data && data.__ob__) {
- "development" !== 'production' && warn(
- "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
- 'Always create fresh vnode data objects in each render!',
- context
- );
- return
- }
- if (!tag) {
- // in case of component :is set to falsy value
- return emptyVNode()
- }
- if (typeof tag === 'string') {
- var Ctor;
- var ns = config.getTagNamespace(tag);
- if (config.isReservedTag(tag)) {
- // platform built-in elements
- return new VNode(
- tag, data, normalizeChildren(children, ns),
- undefined, undefined, ns, context
- )
- } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
- // component
- return createComponent(Ctor, data, context, children, tag)
- } else {
- // unknown or unlisted namespaced elements
- // check at runtime because it may get assigned a namespace when its
- // parent normalizes children
- return new VNode(
- tag, data, normalizeChildren(children, ns),
- undefined, undefined, ns, context
- )
- }
- } else {
- // direct component options / constructor
- return createComponent(tag, data, context, children)
- }
-}
-
-/* */
-
-function initRender (vm) {
- vm.$vnode = null; // the placeholder node in parent tree
- vm._vnode = null; // the root of the child tree
- vm._staticTrees = null;
- vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context;
- vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext);
- // bind the public createElement fn to this instance
- // so that we get proper render context inside it.
- vm.$createElement = bind$1(createElement, vm);
- if (vm.$options.el) {
- vm.$mount(vm.$options.el);
- }
-}
-
-function renderMixin (Vue) {
- Vue.prototype.$nextTick = function (fn) {
- nextTick(fn, this);
- };
-
- Vue.prototype._render = function () {
- var vm = this;
- var ref = vm.$options;
- var render = ref.render;
- var staticRenderFns = ref.staticRenderFns;
- var _parentVnode = ref._parentVnode;
-
- if (vm._isMounted) {
- // clone slot nodes on re-renders
- for (var key in vm.$slots) {
- vm.$slots[key] = cloneVNodes(vm.$slots[key]);
- }
- }
-
- if (staticRenderFns && !vm._staticTrees) {
- vm._staticTrees = [];
- }
- // set parent vnode. this allows render functions to have access
- // to the data on the placeholder node.
- vm.$vnode = _parentVnode;
- // render self
- var vnode;
- try {
- vnode = render.call(vm._renderProxy, vm.$createElement);
- } catch (e) {
- {
- warn(("Error when rendering " + (formatComponentName(vm)) + ":"));
- }
- /* istanbul ignore else */
- if (config.errorHandler) {
- config.errorHandler.call(null, e, vm);
- } else {
- if (config._isServer) {
- throw e
- } else {
- setTimeout(function () { throw e }, 0);
- }
- }
- // return previous vnode to prevent render error causing blank component
- vnode = vm._vnode;
- }
- // return empty vnode in case the render function errored out
- if (!(vnode instanceof VNode)) {
- if ("development" !== 'production' && Array.isArray(vnode)) {
- warn(
- 'Multiple root nodes returned from render function. Render function ' +
- 'should return a single root node.',
- vm
- );
- }
- vnode = emptyVNode();
- }
- // set parent
- vnode.parent = _parentVnode;
- return vnode
- };
-
- // shorthands used in render functions
- Vue.prototype._h = createElement;
- // toString for mustaches
- Vue.prototype._s = _toString;
- // number conversion
- Vue.prototype._n = toNumber;
- // empty vnode
- Vue.prototype._e = emptyVNode;
- // loose equal
- Vue.prototype._q = looseEqual;
- // loose indexOf
- Vue.prototype._i = looseIndexOf;
-
- // render static tree by index
- Vue.prototype._m = function renderStatic (
- index,
- isInFor
- ) {
- var tree = this._staticTrees[index];
- // if has already-rendered static tree and not inside v-for,
- // we can reuse the same tree by doing a shallow clone.
- if (tree && !isInFor) {
- return Array.isArray(tree)
- ? cloneVNodes(tree)
- : cloneVNode(tree)
- }
- // otherwise, render a fresh tree.
- tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy);
- if (Array.isArray(tree)) {
- for (var i = 0; i < tree.length; i++) {
- if (typeof tree[i] !== 'string') {
- tree[i].isStatic = true;
- tree[i].key = "__static__" + index + "_" + i;
- }
- }
- } else {
- tree.isStatic = true;
- tree.key = "__static__" + index;
- }
- return tree
- };
-
- // filter resolution helper
- var identity = function (_) { return _; };
- Vue.prototype._f = function resolveFilter (id) {
- return resolveAsset(this.$options, 'filters', id, true) || identity
- };
-
- // render v-for
- Vue.prototype._l = function renderList (
- val,
- render
- ) {
- var ret, i, l, keys, key;
- if (Array.isArray(val)) {
- ret = new Array(val.length);
- for (i = 0, l = val.length; i < l; i++) {
- ret[i] = render(val[i], i);
- }
- } else if (typeof val === 'number') {
- ret = new Array(val);
- for (i = 0; i < val; i++) {
- ret[i] = render(i + 1, i);
- }
- } else if (isObject(val)) {
- keys = Object.keys(val);
- ret = new Array(keys.length);
- for (i = 0, l = keys.length; i < l; i++) {
- key = keys[i];
- ret[i] = render(val[key], key, i);
- }
- }
- return ret
- };
-
- // renderSlot
- Vue.prototype._t = function (
- name,
- fallback
- ) {
- var slotNodes = this.$slots[name];
- // warn duplicate slot usage
- if (slotNodes && "development" !== 'production') {
- slotNodes._rendered && warn(
- "Duplicate presence of slot \"" + name + "\" found in the same render tree " +
- "- this will likely cause render errors.",
- this
- );
- slotNodes._rendered = true;
- }
- return slotNodes || fallback
- };
-
- // apply v-bind object
- Vue.prototype._b = function bindProps (
- data,
- value,
- asProp
- ) {
- if (value) {
- if (!isObject(value)) {
- "development" !== 'production' && warn(
- 'v-bind without argument expects an Object or Array value',
- this
- );
- } else {
- if (Array.isArray(value)) {
- value = toObject(value);
- }
- for (var key in value) {
- if (key === 'class' || key === 'style') {
- data[key] = value[key];
- } else {
- var hash = asProp || config.mustUseProp(key)
- ? data.domProps || (data.domProps = {})
- : data.attrs || (data.attrs = {});
- hash[key] = value[key];
- }
- }
- }
- }
- return data
- };
-
- // expose v-on keyCodes
- Vue.prototype._k = function getKeyCodes (key) {
- return config.keyCodes[key]
- };
-}
-
-function resolveSlots (
- renderChildren,
- context
-) {
- var slots = {};
- if (!renderChildren) {
- return slots
- }
- var children = normalizeChildren(renderChildren) || [];
- var defaultSlot = [];
- var name, child;
- for (var i = 0, l = children.length; i < l; i++) {
- child = children[i];
- // named slots should only be respected if the vnode was rendered in the
- // same context.
- if ((child.context === context || child.functionalContext === context) &&
- child.data && (name = child.data.slot)) {
- var slot = (slots[name] || (slots[name] = []));
- if (child.tag === 'template') {
- slot.push.apply(slot, child.children);
- } else {
- slot.push(child);
- }
- } else {
- defaultSlot.push(child);
- }
- }
- // ignore single whitespace
- if (defaultSlot.length && !(
- defaultSlot.length === 1 &&
- (defaultSlot[0].text === ' ' || defaultSlot[0].isComment)
- )) {
- slots.default = defaultSlot;
- }
- return slots
-}
-
-/* */
-
-function initEvents (vm) {
- vm._events = Object.create(null);
- // init parent attached events
- var listeners = vm.$options._parentListeners;
- var on = bind$1(vm.$on, vm);
- var off = bind$1(vm.$off, vm);
- vm._updateListeners = function (listeners, oldListeners) {
- updateListeners(listeners, oldListeners || {}, on, off, vm);
- };
- if (listeners) {
- vm._updateListeners(listeners);
- }
-}
-
-function eventsMixin (Vue) {
- Vue.prototype.$on = function (event, fn) {
- var vm = this;(vm._events[event] || (vm._events[event] = [])).push(fn);
- return vm
- };
-
- Vue.prototype.$once = function (event, fn) {
- var vm = this;
- function on () {
- vm.$off(event, on);
- fn.apply(vm, arguments);
- }
- on.fn = fn;
- vm.$on(event, on);
- return vm
- };
-
- Vue.prototype.$off = function (event, fn) {
- var vm = this;
- // all
- if (!arguments.length) {
- vm._events = Object.create(null);
- return vm
- }
- // specific event
- var cbs = vm._events[event];
- if (!cbs) {
- return vm
- }
- if (arguments.length === 1) {
- vm._events[event] = null;
- return vm
- }
- // specific handler
- var cb;
- var i = cbs.length;
- while (i--) {
- cb = cbs[i];
- if (cb === fn || cb.fn === fn) {
- cbs.splice(i, 1);
- break
- }
- }
- return vm
- };
-
- Vue.prototype.$emit = function (event) {
- var vm = this;
- var cbs = vm._events[event];
- if (cbs) {
- cbs = cbs.length > 1 ? toArray(cbs) : cbs;
- var args = toArray(arguments, 1);
- for (var i = 0, l = cbs.length; i < l; i++) {
- cbs[i].apply(vm, args);
- }
- }
- return vm
- };
-}
-
-/* */
-
-var uid = 0;
-
-function initMixin (Vue) {
- Vue.prototype._init = function (options) {
- var vm = this;
- // a uid
- vm._uid = uid++;
- // a flag to avoid this being observed
- vm._isVue = true;
- // merge options
- if (options && options._isComponent) {
- // optimize internal component instantiation
- // since dynamic options merging is pretty slow, and none of the
- // internal component options needs special treatment.
- initInternalComponent(vm, options);
- } else {
- vm.$options = mergeOptions(
- resolveConstructorOptions(vm),
- options || {},
- vm
- );
- }
- /* istanbul ignore else */
- {
- initProxy(vm);
- }
- // expose real self
- vm._self = vm;
- initLifecycle(vm);
- initEvents(vm);
- callHook(vm, 'beforeCreate');
- initState(vm);
- callHook(vm, 'created');
- initRender(vm);
- };
-
- function initInternalComponent (vm, options) {
- var opts = vm.$options = Object.create(resolveConstructorOptions(vm));
- // doing this because it's faster than dynamic enumeration.
- opts.parent = options.parent;
- opts.propsData = options.propsData;
- opts._parentVnode = options._parentVnode;
- opts._parentListeners = options._parentListeners;
- opts._renderChildren = options._renderChildren;
- opts._componentTag = options._componentTag;
- if (options.render) {
- opts.render = options.render;
- opts.staticRenderFns = options.staticRenderFns;
- }
- }
-
- function resolveConstructorOptions (vm) {
- var Ctor = vm.constructor;
- var options = Ctor.options;
- if (Ctor.super) {
- var superOptions = Ctor.super.options;
- var cachedSuperOptions = Ctor.superOptions;
- if (superOptions !== cachedSuperOptions) {
- // super option changed
- Ctor.superOptions = superOptions;
- options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
- if (options.name) {
- options.components[options.name] = Ctor;
- }
- }
- }
- return options
- }
-}
-
-function Vue$3 (options) {
- if ("development" !== 'production' &&
- !(this instanceof Vue$3)) {
- warn('Vue is a constructor and should be called with the `new` keyword');
- }
- this._init(options);
-}
-
-initMixin(Vue$3);
-stateMixin(Vue$3);
-eventsMixin(Vue$3);
-lifecycleMixin(Vue$3);
-renderMixin(Vue$3);
-
-var warn = noop;
-var formatComponentName;
-
-{
- var hasConsole = typeof console !== 'undefined';
-
- warn = function (msg, vm) {
- if (hasConsole && (!config.silent)) {
- console.error("[Vue warn]: " + msg + " " + (
- vm ? formatLocation(formatComponentName(vm)) : ''
- ));
- }
- };
-
- formatComponentName = function (vm) {
- if (vm.$root === vm) {
- return 'root instance'
- }
- var name = vm._isVue
- ? vm.$options.name || vm.$options._componentTag
- : vm.name;
- return (
- (name ? ("component <" + name + ">") : "anonymous component") +
- (vm._isVue && vm.$options.__file ? (" at " + (vm.$options.__file)) : '')
- )
- };
-
- var formatLocation = function (str) {
- if (str === 'anonymous component') {
- str += " - use the \"name\" option for better debugging messages.";
- }
- return ("\n(found in " + str + ")")
- };
-}
-
-/* */
-
-/**
- * Option overwriting strategies are functions that handle
- * how to merge a parent option value and a child option
- * value into the final value.
- */
-var strats = config.optionMergeStrategies;
-
-/**
- * Options with restrictions
- */
-{
- strats.el = strats.propsData = function (parent, child, vm, key) {
- if (!vm) {
- warn(
- "option \"" + key + "\" can only be used during instance " +
- 'creation with the `new` keyword.'
- );
- }
- return defaultStrat(parent, child)
- };
-}
-
-/**
- * Helper that recursively merges two data objects together.
- */
-function mergeData (to, from) {
- var key, toVal, fromVal;
- for (key in from) {
- toVal = to[key];
- fromVal = from[key];
- if (!hasOwn(to, key)) {
- set(to, key, fromVal);
- } else if (isObject(toVal) && isObject(fromVal)) {
- mergeData(toVal, fromVal);
- }
- }
- return to
-}
-
-/**
- * Data
- */
-strats.data = function (
- parentVal,
- childVal,
- vm
-) {
- if (!vm) {
- // in a Vue.extend merge, both should be functions
- if (!childVal) {
- return parentVal
- }
- if (typeof childVal !== 'function') {
- "development" !== 'production' && warn(
- 'The "data" option should be a function ' +
- 'that returns a per-instance value in component ' +
- 'definitions.',
- vm
- );
- return parentVal
- }
- if (!parentVal) {
- return childVal
- }
- // when parentVal & childVal are both present,
- // we need to return a function that returns the
- // merged result of both functions... no need to
- // check if parentVal is a function here because
- // it has to be a function to pass previous merges.
- return function mergedDataFn () {
- return mergeData(
- childVal.call(this),
- parentVal.call(this)
- )
- }
- } else if (parentVal || childVal) {
- return function mergedInstanceDataFn () {
- // instance merge
- var instanceData = typeof childVal === 'function'
- ? childVal.call(vm)
- : childVal;
- var defaultData = typeof parentVal === 'function'
- ? parentVal.call(vm)
- : undefined;
- if (instanceData) {
- return mergeData(instanceData, defaultData)
- } else {
- return defaultData
- }
- }
- }
-};
-
-/**
- * Hooks and param attributes are merged as arrays.
- */
-function mergeHook (
- parentVal,
- childVal
-) {
- return childVal
- ? parentVal
- ? parentVal.concat(childVal)
- : Array.isArray(childVal)
- ? childVal
- : [childVal]
- : parentVal
-}
-
-config._lifecycleHooks.forEach(function (hook) {
- strats[hook] = mergeHook;
-});
-
-/**
- * Assets
- *
- * When a vm is present (instance creation), we need to do
- * a three-way merge between constructor options, instance
- * options and parent options.
- */
-function mergeAssets (parentVal, childVal) {
- var res = Object.create(parentVal || null);
- return childVal
- ? extend(res, childVal)
- : res
-}
-
-config._assetTypes.forEach(function (type) {
- strats[type + 's'] = mergeAssets;
-});
-
-/**
- * Watchers.
- *
- * Watchers hashes should not overwrite one
- * another, so we merge them as arrays.
- */
-strats.watch = function (parentVal, childVal) {
- /* istanbul ignore if */
- if (!childVal) { return parentVal }
- if (!parentVal) { return childVal }
- var ret = {};
- extend(ret, parentVal);
- for (var key in childVal) {
- var parent = ret[key];
- var child = childVal[key];
- if (parent && !Array.isArray(parent)) {
- parent = [parent];
- }
- ret[key] = parent
- ? parent.concat(child)
- : [child];
- }
- return ret
-};
-
-/**
- * Other object hashes.
- */
-strats.props =
-strats.methods =
-strats.computed = function (parentVal, childVal) {
- if (!childVal) { return parentVal }
- if (!parentVal) { return childVal }
- var ret = Object.create(null);
- extend(ret, parentVal);
- extend(ret, childVal);
- return ret
-};
-
-/**
- * Default strategy.
- */
-var defaultStrat = function (parentVal, childVal) {
- return childVal === undefined
- ? parentVal
- : childVal
-};
-
-/**
- * Make sure component options get converted to actual
- * constructors.
- */
-function normalizeComponents (options) {
- if (options.components) {
- var components = options.components;
- var def;
- for (var key in components) {
- var lower = key.toLowerCase();
- if (isBuiltInTag(lower) || config.isReservedTag(lower)) {
- "development" !== 'production' && warn(
- 'Do not use built-in or reserved HTML elements as component ' +
- 'id: ' + key
- );
- continue
- }
- def = components[key];
- if (isPlainObject(def)) {
- components[key] = Vue$3.extend(def);
- }
- }
- }
-}
-
-/**
- * Ensure all props option syntax are normalized into the
- * Object-based format.
- */
-function normalizeProps (options) {
- var props = options.props;
- if (!props) { return }
- var res = {};
- var i, val, name;
- if (Array.isArray(props)) {
- i = props.length;
- while (i--) {
- val = props[i];
- if (typeof val === 'string') {
- name = camelize(val);
- res[name] = { type: null };
- } else {
- warn('props must be strings when using array syntax.');
- }
- }
- } else if (isPlainObject(props)) {
- for (var key in props) {
- val = props[key];
- name = camelize(key);
- res[name] = isPlainObject(val)
- ? val
- : { type: val };
- }
- }
- options.props = res;
-}
-
-/**
- * Normalize raw function directives into object format.
- */
-function normalizeDirectives (options) {
- var dirs = options.directives;
- if (dirs) {
- for (var key in dirs) {
- var def = dirs[key];
- if (typeof def === 'function') {
- dirs[key] = { bind: def, update: def };
- }
- }
- }
-}
-
-/**
- * Merge two option objects into a new one.
- * Core utility used in both instantiation and inheritance.
- */
-function mergeOptions (
- parent,
- child,
- vm
-) {
- normalizeComponents(child);
- normalizeProps(child);
- normalizeDirectives(child);
- var extendsFrom = child.extends;
- if (extendsFrom) {
- parent = typeof extendsFrom === 'function'
- ? mergeOptions(parent, extendsFrom.options, vm)
- : mergeOptions(parent, extendsFrom, vm);
- }
- if (child.mixins) {
- for (var i = 0, l = child.mixins.length; i < l; i++) {
- var mixin = child.mixins[i];
- if (mixin.prototype instanceof Vue$3) {
- mixin = mixin.options;
- }
- parent = mergeOptions(parent, mixin, vm);
- }
- }
- var options = {};
- var key;
- for (key in parent) {
- mergeField(key);
- }
- for (key in child) {
- if (!hasOwn(parent, key)) {
- mergeField(key);
- }
- }
- function mergeField (key) {
- var strat = strats[key] || defaultStrat;
- options[key] = strat(parent[key], child[key], vm, key);
- }
- return options
-}
-
-/**
- * Resolve an asset.
- * This function is used because child instances need access
- * to assets defined in its ancestor chain.
- */
-function resolveAsset (
- options,
- type,
- id,
- warnMissing
-) {
- /* istanbul ignore if */
- if (typeof id !== 'string') {
- return
- }
- var assets = options[type];
- var res = assets[id] ||
- // camelCase ID
- assets[camelize(id)] ||
- // Pascal Case ID
- assets[capitalize(camelize(id))];
- if ("development" !== 'production' && warnMissing && !res) {
- warn(
- 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
- options
- );
- }
- return res
-}
-
-/* */
-
-function validateProp (
- key,
- propOptions,
- propsData,
- vm
-) {
- var prop = propOptions[key];
- var absent = !hasOwn(propsData, key);
- var value = propsData[key];
- // handle boolean props
- if (isBooleanType(prop.type)) {
- if (absent && !hasOwn(prop, 'default')) {
- value = false;
- } else if (value === '' || value === hyphenate(key)) {
- value = true;
- }
- }
- // check default value
- if (value === undefined) {
- value = getPropDefaultValue(vm, prop, key);
- // since the default value is a fresh copy,
- // make sure to observe it.
- var prevShouldConvert = observerState.shouldConvert;
- observerState.shouldConvert = true;
- observe(value);
- observerState.shouldConvert = prevShouldConvert;
- }
- {
- assertProp(prop, key, value, vm, absent);
- }
- return value
-}
-
-/**
- * Get the default value of a prop.
- */
-function getPropDefaultValue (vm, prop, name) {
- // no default, return undefined
- if (!hasOwn(prop, 'default')) {
- return undefined
- }
- var def = prop.default;
- // warn against non-factory defaults for Object & Array
- if (isObject(def)) {
- "development" !== 'production' && warn(
- 'Invalid default value for prop "' + name + '": ' +
- 'Props with type Object/Array must use a factory function ' +
- 'to return the default value.',
- vm
- );
- }
- // call factory function for non-Function types
- return typeof def === 'function' && prop.type !== Function
- ? def.call(vm)
- : def
-}
-
-/**
- * Assert whether a prop is valid.
- */
-function assertProp (
- prop,
- name,
- value,
- vm,
- absent
-) {
- if (prop.required && absent) {
- warn(
- 'Missing required prop: "' + name + '"',
- vm
- );
- return
- }
- if (value == null && !prop.required) {
- return
- }
- var type = prop.type;
- var valid = !type || type === true;
- var expectedTypes = [];
- if (type) {
- if (!Array.isArray(type)) {
- type = [type];
- }
- for (var i = 0; i < type.length && !valid; i++) {
- var assertedType = assertType(value, type[i]);
- expectedTypes.push(assertedType.expectedType);
- valid = assertedType.valid;
- }
- }
- if (!valid) {
- warn(
- 'Invalid prop: type check failed for prop "' + name + '".' +
- ' Expected ' + expectedTypes.map(capitalize).join(', ') +
- ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.',
- vm
- );
- return
- }
- var validator = prop.validator;
- if (validator) {
- if (!validator(value)) {
- warn(
- 'Invalid prop: custom validator check failed for prop "' + name + '".',
- vm
- );
- }
- }
-}
-
-/**
- * Assert the type of a value
- */
-function assertType (value, type) {
- var valid;
- var expectedType = getType(type);
- if (expectedType === 'String') {
- valid = typeof value === (expectedType = 'string');
- } else if (expectedType === 'Number') {
- valid = typeof value === (expectedType = 'number');
- } else if (expectedType === 'Boolean') {
- valid = typeof value === (expectedType = 'boolean');
- } else if (expectedType === 'Function') {
- valid = typeof value === (expectedType = 'function');
- } else if (expectedType === 'Object') {
- valid = isPlainObject(value);
- } else if (expectedType === 'Array') {
- valid = Array.isArray(value);
- } else {
- valid = value instanceof type;
- }
- return {
- valid: valid,
- expectedType: expectedType
- }
-}
-
-/**
- * Use function string name to check built-in types,
- * because a simple equality check will fail when running
- * across different vms / iframes.
- */
-function getType (fn) {
- var match = fn && fn.toString().match(/^\s*function (\w+)/);
- return match && match[1]
-}
-
-function isBooleanType (fn) {
- if (!Array.isArray(fn)) {
- return getType(fn) === 'Boolean'
- }
- for (var i = 0, len = fn.length; i < len; i++) {
- if (getType(fn[i]) === 'Boolean') {
- return true
- }
- }
- /* istanbul ignore next */
- return false
-}
-
-
-
-var util = Object.freeze({
- defineReactive: defineReactive$$1,
- _toString: _toString,
- toNumber: toNumber,
- makeMap: makeMap,
- isBuiltInTag: isBuiltInTag,
- remove: remove$1,
- hasOwn: hasOwn,
- isPrimitive: isPrimitive,
- cached: cached,
- camelize: camelize,
- capitalize: capitalize,
- hyphenate: hyphenate,
- bind: bind$1,
- toArray: toArray,
- extend: extend,
- isObject: isObject,
- isPlainObject: isPlainObject,
- toObject: toObject,
- noop: noop,
- no: no,
- genStaticKeys: genStaticKeys,
- looseEqual: looseEqual,
- looseIndexOf: looseIndexOf,
- isReserved: isReserved,
- def: def,
- parsePath: parsePath,
- hasProto: hasProto,
- inBrowser: inBrowser,
- UA: UA,
- isIE: isIE,
- isIE9: isIE9,
- isEdge: isEdge,
- isAndroid: isAndroid,
- isIOS: isIOS,
- devtools: devtools,
- nextTick: nextTick,
- get _Set () { return _Set; },
- mergeOptions: mergeOptions,
- resolveAsset: resolveAsset,
- get warn () { return warn; },
- get formatComponentName () { return formatComponentName; },
- validateProp: validateProp
-});
-
-/* */
-
-function initUse (Vue) {
- Vue.use = function (plugin) {
- /* istanbul ignore if */
- if (plugin.installed) {
- return
- }
- // additional parameters
- var args = toArray(arguments, 1);
- args.unshift(this);
- if (typeof plugin.install === 'function') {
- plugin.install.apply(plugin, args);
- } else {
- plugin.apply(null, args);
- }
- plugin.installed = true;
- return this
- };
-}
-
-/* */
-
-function initMixin$1 (Vue) {
- Vue.mixin = function (mixin) {
- Vue.options = mergeOptions(Vue.options, mixin);
- };
-}
-
-/* */
-
-function initExtend (Vue) {
- /**
- * Each instance constructor, including Vue, has a unique
- * cid. This enables us to create wrapped "child
- * constructors" for prototypal inheritance and cache them.
- */
- Vue.cid = 0;
- var cid = 1;
-
- /**
- * Class inheritance
- */
- Vue.extend = function (extendOptions) {
- extendOptions = extendOptions || {};
- var Super = this;
- var isFirstExtend = Super.cid === 0;
- if (isFirstExtend && extendOptions._Ctor) {
- return extendOptions._Ctor
- }
- var name = extendOptions.name || Super.options.name;
- {
- if (!/^[a-zA-Z][\w-]*$/.test(name)) {
- warn(
- 'Invalid component name: "' + name + '". Component names ' +
- 'can only contain alphanumeric characaters and the hyphen.'
- );
- name = null;
- }
- }
- var Sub = function VueComponent (options) {
- this._init(options);
- };
- Sub.prototype = Object.create(Super.prototype);
- Sub.prototype.constructor = Sub;
- Sub.cid = cid++;
- Sub.options = mergeOptions(
- Super.options,
- extendOptions
- );
- Sub['super'] = Super;
- // allow further extension
- Sub.extend = Super.extend;
- // create asset registers, so extended classes
- // can have their private assets too.
- config._assetTypes.forEach(function (type) {
- Sub[type] = Super[type];
- });
- // enable recursive self-lookup
- if (name) {
- Sub.options.components[name] = Sub;
- }
- // keep a reference to the super options at extension time.
- // later at instantiation we can check if Super's options have
- // been updated.
- Sub.superOptions = Super.options;
- Sub.extendOptions = extendOptions;
- // cache constructor
- if (isFirstExtend) {
- extendOptions._Ctor = Sub;
- }
- return Sub
- };
-}
-
-/* */
-
-function initAssetRegisters (Vue) {
- /**
- * Create asset registration methods.
- */
- config._assetTypes.forEach(function (type) {
- Vue[type] = function (
- id,
- definition
- ) {
- if (!definition) {
- return this.options[type + 's'][id]
- } else {
- /* istanbul ignore if */
- {
- if (type === 'component' && config.isReservedTag(id)) {
- warn(
- 'Do not use built-in or reserved HTML elements as component ' +
- 'id: ' + id
- );
- }
- }
- if (type === 'component' && isPlainObject(definition)) {
- definition.name = definition.name || id;
- definition = Vue.extend(definition);
- }
- if (type === 'directive' && typeof definition === 'function') {
- definition = { bind: definition, update: definition };
- }
- this.options[type + 's'][id] = definition;
- return definition
- }
- };
- });
-}
-
-var KeepAlive = {
- name: 'keep-alive',
- abstract: true,
- created: function created () {
- this.cache = Object.create(null);
- },
- render: function render () {
- var vnode = getFirstComponentChild(this.$slots.default);
- if (vnode && vnode.componentOptions) {
- var opts = vnode.componentOptions;
- var key = vnode.key == null
- // same constructor may get registered as different local components
- // so cid alone is not enough (#3269)
- ? opts.Ctor.cid + '::' + opts.tag
- : vnode.key;
- if (this.cache[key]) {
- vnode.child = this.cache[key].child;
- } else {
- this.cache[key] = vnode;
- }
- vnode.data.keepAlive = true;
- }
- return vnode
- },
- destroyed: function destroyed () {
- var this$1 = this;
-
- for (var key in this.cache) {
- var vnode = this$1.cache[key];
- callHook(vnode.child, 'deactivated');
- vnode.child.$destroy();
- }
- }
-};
-
-var builtInComponents = {
- KeepAlive: KeepAlive
-};
-
-/* */
-
-function initGlobalAPI (Vue) {
- // config
- var configDef = {};
- configDef.get = function () { return config; };
- {
- configDef.set = function () {
- warn(
- 'Do not replace the Vue.config object, set individual fields instead.'
- );
- };
- }
- Object.defineProperty(Vue, 'config', configDef);
- Vue.util = util;
- Vue.set = set;
- Vue.delete = del;
- Vue.nextTick = nextTick;
-
- Vue.options = Object.create(null);
- config._assetTypes.forEach(function (type) {
- Vue.options[type + 's'] = Object.create(null);
- });
-
- extend(Vue.options.components, builtInComponents);
-
- initUse(Vue);
- initMixin$1(Vue);
- initExtend(Vue);
- initAssetRegisters(Vue);
-}
-
-initGlobalAPI(Vue$3);
-
-Object.defineProperty(Vue$3.prototype, '$isServer', {
- get: function () { return config._isServer; }
-});
-
-Vue$3.version = '2.0.3';
-
-/* */
-
-// attributes that should be using props for binding
-var mustUseProp = makeMap('value,selected,checked,muted');
-
-var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');
-
-var isBooleanAttr = makeMap(
- 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +
- 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +
- 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +
- 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +
- 'required,reversed,scoped,seamless,selected,sortable,translate,' +
- 'truespeed,typemustmatch,visible'
-);
-
-var isAttr = makeMap(
- 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
- 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
- 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
- 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
- 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
- 'form,formaction,headers,<th>,height,hidden,high,href,hreflang,http-equiv,' +
- 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
- 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
- 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
- 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
- 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
- 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
- 'target,title,type,usemap,value,width,wrap'
-);
-
-
-
-var xlinkNS = 'http://www.w3.org/1999/xlink';
-
-var isXlink = function (name) {
- return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'
-};
-
-var getXlinkProp = function (name) {
- return isXlink(name) ? name.slice(6, name.length) : ''
-};
-
-var isFalsyAttrValue = function (val) {
- return val == null || val === false
-};
-
-/* */
-
-function genClassForVnode (vnode) {
- var data = vnode.data;
- var parentNode = vnode;
- var childNode = vnode;
- while (childNode.child) {
- childNode = childNode.child._vnode;
- if (childNode.data) {
- data = mergeClassData(childNode.data, data);
- }
- }
- while ((parentNode = parentNode.parent)) {
- if (parentNode.data) {
- data = mergeClassData(data, parentNode.data);
- }
- }
- return genClassFromData(data)
-}
-
-function mergeClassData (child, parent) {
- return {
- staticClass: concat(child.staticClass, parent.staticClass),
- class: child.class
- ? [child.class, parent.class]
- : parent.class
- }
-}
-
-function genClassFromData (data) {
- var dynamicClass = data.class;
- var staticClass = data.staticClass;
- if (staticClass || dynamicClass) {
- return concat(staticClass, stringifyClass(dynamicClass))
- }
- /* istanbul ignore next */
- return ''
-}
-
-function concat (a, b) {
- return a ? b ? (a + ' ' + b) : a : (b || '')
-}
-
-function stringifyClass (value) {
- var res = '';
- if (!value) {
- return res
- }
- if (typeof value === 'string') {
- return value
- }
- if (Array.isArray(value)) {
- var stringified;
- for (var i = 0, l = value.length; i < l; i++) {
- if (value[i]) {
- if ((stringified = stringifyClass(value[i]))) {
- res += stringified + ' ';
- }
- }
- }
- return res.slice(0, -1)
- }
- if (isObject(value)) {
- for (var key in value) {
- if (value[key]) { res += key + ' '; }
- }
- return res.slice(0, -1)
- }
- /* istanbul ignore next */
- return res
-}
-
-/* */
-
-var namespaceMap = {
- svg: 'http://www.w3.org/2000/svg',
- math: 'http://www.w3.org/1998/Math/MathML'
-};
-
-var isHTMLTag = makeMap(
- 'html,body,base,head,link,meta,style,title,' +
- 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
- 'div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,' +
- 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
- 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
- 'embed,object,param,source,canvas,script,noscript,del,ins,' +
- 'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
- 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
- 'output,progress,select,textarea,' +
- 'details,dialog,menu,menuitem,summary,' +
- 'content,element,shadow,template'
-);
-
-var isUnaryTag = makeMap(
- 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
- 'link,meta,param,source,track,wbr',
- true
-);
-
-// Elements that you can, intentionally, leave open
-// (and which close themselves)
-var canBeLeftOpenTag = makeMap(
- 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source',
- true
-);
-
-// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
-// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
-var isNonPhrasingTag = makeMap(
- 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
- 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
- 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
- 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
- 'title,tr,track',
- true
-);
-
-// this map is intentionally selective, only covering SVG elements that may
-// contain child elements.
-var isSVG = makeMap(
- 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,' +
- 'font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
- 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
- true
-);
-
-var isPreTag = function (tag) { return tag === 'pre'; };
-
-var isReservedTag = function (tag) {
- return isHTMLTag(tag) || isSVG(tag)
-};
-
-function getTagNamespace (tag) {
- if (isSVG(tag)) {
- return 'svg'
- }
- // basic support for MathML
- // note it doesn't support other MathML elements being component roots
- if (tag === 'math') {
- return 'math'
- }
-}
-
-var unknownElementCache = Object.create(null);
-function isUnknownElement (tag) {
- /* istanbul ignore if */
- if (!inBrowser) {
- return true
- }
- if (isReservedTag(tag)) {
- return false
- }
- tag = tag.toLowerCase();
- /* istanbul ignore if */
- if (unknownElementCache[tag] != null) {
- return unknownElementCache[tag]
- }
- var el = document.createElement(tag);
- if (tag.indexOf('-') > -1) {
- // http://stackoverflow.com/a/28210364/1070244
- return (unknownElementCache[tag] = (
- el.constructor === window.HTMLUnknownElement ||
- el.constructor === window.HTMLElement
- ))
- } else {
- return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))
- }
-}
-
-/* */
-
-/**
- * Query an element selector if it's not an element already.
- */
-function query (el) {
- if (typeof el === 'string') {
- var selector = el;
- el = document.querySelector(el);
- if (!el) {
- "development" !== 'production' && warn(
- 'Cannot find element: ' + selector
- );
- return document.createElement('div')
- }
- }
- return el
-}
-
-/* */
-
-function createElement$1 (tagName, vnode) {
- var elm = document.createElement(tagName);
- if (tagName !== 'select') {
- return elm
- }
- if (vnode.data && vnode.data.attrs && 'multiple' in vnode.data.attrs) {
- elm.setAttribute('multiple', 'multiple');
- }
- return elm
-}
-
-function createElementNS (namespace, tagName) {
- return document.createElementNS(namespaceMap[namespace], tagName)
-}
-
-function createTextNode (text) {
- return document.createTextNode(text)
-}
-
-function createComment (text) {
- return document.createComment(text)
-}
-
-function insertBefore (parentNode, newNode, referenceNode) {
- parentNode.insertBefore(newNode, referenceNode);
-}
-
-function removeChild (node, child) {
- node.removeChild(child);
-}
-
-function appendChild (node, child) {
- node.appendChild(child);
-}
-
-function parentNode (node) {
- return node.parentNode
-}
-
-function nextSibling (node) {
- return node.nextSibling
-}
-
-function tagName (node) {
- return node.tagName
-}
-
-function setTextContent (node, text) {
- node.textContent = text;
-}
-
-function childNodes (node) {
- return node.childNodes
-}
-
-function setAttribute (node, key, val) {
- node.setAttribute(key, val);
-}
-
-
-var nodeOps = Object.freeze({
- createElement: createElement$1,
- createElementNS: createElementNS,
- createTextNode: createTextNode,
- createComment: createComment,
- insertBefore: insertBefore,
- removeChild: removeChild,
- appendChild: appendChild,
- parentNode: parentNode,
- nextSibling: nextSibling,
- tagName: tagName,
- setTextContent: setTextContent,
- childNodes: childNodes,
- setAttribute: setAttribute
-});
-
-/* */
-
-var ref = {
- create: function create (_, vnode) {
- registerRef(vnode);
- },
- update: function update (oldVnode, vnode) {
- if (oldVnode.data.ref !== vnode.data.ref) {
- registerRef(oldVnode, true);
- registerRef(vnode);
- }
- },
- destroy: function destroy (vnode) {
- registerRef(vnode, true);
- }
-};
-
-function registerRef (vnode, isRemoval) {
- var key = vnode.data.ref;
- if (!key) { return }
-
- var vm = vnode.context;
- var ref = vnode.child || vnode.elm;
- var refs = vm.$refs;
- if (isRemoval) {
- if (Array.isArray(refs[key])) {
- remove$1(refs[key], ref);
- } else if (refs[key] === ref) {
- refs[key] = undefined;
- }
- } else {
- if (vnode.data.refInFor) {
- if (Array.isArray(refs[key])) {
- refs[key].push(ref);
- } else {
- refs[key] = [ref];
- }
- } else {
- refs[key] = ref;
- }
- }
-}
-
-/**
- * Virtual DOM patching algorithm based on Snabbdom by
- * Simon Friis Vindum (@paldepind)
- * Licensed under the MIT License
- * https://github.com/paldepind/snabbdom/blob/master/LICENSE
- *
- * modified by Evan You (@yyx990803)
- *
-
-/*
- * Not type-checking this because this file is perf-critical and the cost
- * of making flow understand it is not worth it.
- */
-
-var emptyNode = new VNode('', {}, []);
-
-var hooks$1 = ['create', 'update', 'remove', 'destroy'];
-
-function isUndef (s) {
- return s == null
-}
-
-function isDef (s) {
- return s != null
-}
-
-function sameVnode (vnode1, vnode2) {
- return (
- vnode1.key === vnode2.key &&
- vnode1.tag === vnode2.tag &&
- vnode1.isComment === vnode2.isComment &&
- !vnode1.data === !vnode2.data
- )
-}
-
-function createKeyToOldIdx (children, beginIdx, endIdx) {
- var i, key;
- var map = {};
- for (i = beginIdx; i <= endIdx; ++i) {
- key = children[i].key;
- if (isDef(key)) { map[key] = i; }
- }
- return map
-}
-
-function createPatchFunction (backend) {
- var i, j;
- var cbs = {};
-
- var modules = backend.modules;
- var nodeOps = backend.nodeOps;
-
- for (i = 0; i < hooks$1.length; ++i) {
- cbs[hooks$1[i]] = [];
- for (j = 0; j < modules.length; ++j) {
- if (modules[j][hooks$1[i]] !== undefined) { cbs[hooks$1[i]].push(modules[j][hooks$1[i]]); }
- }
- }
-
- function emptyNodeAt (elm) {
- return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
- }
-
- function createRmCb (childElm, listeners) {
- function remove$$1 () {
- if (--remove$$1.listeners === 0) {
- removeElement(childElm);
- }
- }
- remove$$1.listeners = listeners;
- return remove$$1
- }
-
- function removeElement (el) {
- var parent = nodeOps.parentNode(el);
- nodeOps.removeChild(parent, el);
- }
-
- function createElm (vnode, insertedVnodeQueue, nested) {
- var i;
- var data = vnode.data;
- vnode.isRootInsert = !nested;
- if (isDef(data)) {
- if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); }
- // after calling the init hook, if the vnode is a child component
- // it should've created a child instance and mounted it. the child
- // component also has set the placeholder vnode's elm.
- // in that case we can just return the element and be done.
- if (isDef(i = vnode.child)) {
- initComponent(vnode, insertedVnodeQueue);
- return vnode.elm
- }
- }
- var children = vnode.children;
- var tag = vnode.tag;
- if (isDef(tag)) {
- {
- if (
- !vnode.ns &&
- !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) &&
- config.isUnknownElement(tag)
- ) {
- warn(
- 'Unknown custom element: <' + tag + '> - did you ' +
- 'register the component correctly? For recursive components, ' +
- 'make sure to provide the "name" option.',
- vnode.context
- );
- }
- }
- vnode.elm = vnode.ns
- ? nodeOps.createElementNS(vnode.ns, tag)
- : nodeOps.createElement(tag, vnode);
- setScope(vnode);
- createChildren(vnode, children, insertedVnodeQueue);
- if (isDef(data)) {
- invokeCreateHooks(vnode, insertedVnodeQueue);
- }
- } else if (vnode.isComment) {
- vnode.elm = nodeOps.createComment(vnode.text);
- } else {
- vnode.elm = nodeOps.createTextNode(vnode.text);
- }
- return vnode.elm
- }
-
- function createChildren (vnode, children, insertedVnodeQueue) {
- if (Array.isArray(children)) {
- for (var i = 0; i < children.length; ++i) {
- nodeOps.appendChild(vnode.elm, createElm(children[i], insertedVnodeQueue, true));
- }
- } else if (isPrimitive(vnode.text)) {
- nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
- }
- }
-
- function isPatchable (vnode) {
- while (vnode.child) {
- vnode = vnode.child._vnode;
- }
- return isDef(vnode.tag)
- }
-
- function invokeCreateHooks (vnode, insertedVnodeQueue) {
- for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
- cbs.create[i$1](emptyNode, vnode);
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create) { i.create(emptyNode, vnode); }
- if (i.insert) { insertedVnodeQueue.push(vnode); }
- }
- }
-
- function initComponent (vnode, insertedVnodeQueue) {
- if (vnode.data.pendingInsert) {
- insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
- }
- vnode.elm = vnode.child.$el;
- if (isPatchable(vnode)) {
- invokeCreateHooks(vnode, insertedVnodeQueue);
- setScope(vnode);
- } else {
- // empty component root.
- // skip all element-related modules except for ref (#3455)
- registerRef(vnode);
- // make sure to invoke the insert hook
- insertedVnodeQueue.push(vnode);
- }
- }
-
- // set scope id attribute for scoped CSS.
- // this is implemented as a special case to avoid the overhead
- // of going through the normal attribute patching process.
- function setScope (vnode) {
- var i;
- if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) {
- nodeOps.setAttribute(vnode.elm, i, '');
- }
- if (isDef(i = activeInstance) &&
- i !== vnode.context &&
- isDef(i = i.$options._scopeId)) {
- nodeOps.setAttribute(vnode.elm, i, '');
- }
- }
-
- function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
- }
- }
-
- function invokeDestroyHook (vnode) {
- var i, j;
- var data = vnode.data;
- if (isDef(data)) {
- if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
- for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
- }
- if (isDef(i = vnode.children)) {
- for (j = 0; j < vnode.children.length; ++j) {
- invokeDestroyHook(vnode.children[j]);
- }
- }
- }
-
- function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
- for (; startIdx <= endIdx; ++startIdx) {
- var ch = vnodes[startIdx];
- if (isDef(ch)) {
- if (isDef(ch.tag)) {
- removeAndInvokeRemoveHook(ch);
- invokeDestroyHook(ch);
- } else { // Text node
- nodeOps.removeChild(parentElm, ch.elm);
- }
- }
- }
- }
-
- function removeAndInvokeRemoveHook (vnode, rm) {
- if (rm || isDef(vnode.data)) {
- var listeners = cbs.remove.length + 1;
- if (!rm) {
- // directly removing
- rm = createRmCb(vnode.elm, listeners);
- } else {
- // we have a recursively passed down rm callback
- // increase the listeners count
- rm.listeners += listeners;
- }
- // recursively invoke hooks on child component root node
- if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
- removeAndInvokeRemoveHook(i, rm);
- }
- for (i = 0; i < cbs.remove.length; ++i) {
- cbs.remove[i](vnode, rm);
- }
- if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
- i(vnode, rm);
- } else {
- rm();
- }
- } else {
- removeElement(vnode.elm);
- }
- }
-
- function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
- var oldStartIdx = 0;
- var newStartIdx = 0;
- var oldEndIdx = oldCh.length - 1;
- var oldStartVnode = oldCh[0];
- var oldEndVnode = oldCh[oldEndIdx];
- var newEndIdx = newCh.length - 1;
- var newStartVnode = newCh[0];
- var newEndVnode = newCh[newEndIdx];
- var oldKeyToIdx, idxInOld, elmToMove, before;
-
- // removeOnly is a special flag used only by <transition-group>
- // to ensure removed elements stay in correct relative positions
- // during leaving transitions
- var canMove = !removeOnly;
-
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
- if (isUndef(oldStartVnode)) {
- oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
- } else if (isUndef(oldEndVnode)) {
- oldEndVnode = oldCh[--oldEndIdx];
- } else if (sameVnode(oldStartVnode, newStartVnode)) {
- patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
- oldStartVnode = oldCh[++oldStartIdx];
- newStartVnode = newCh[++newStartIdx];
- } else if (sameVnode(oldEndVnode, newEndVnode)) {
- patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
- oldEndVnode = oldCh[--oldEndIdx];
- newEndVnode = newCh[--newEndIdx];
- } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
- patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
- canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
- oldStartVnode = oldCh[++oldStartIdx];
- newEndVnode = newCh[--newEndIdx];
- } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
- patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
- canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode = oldCh[--oldEndIdx];
- newStartVnode = newCh[++newStartIdx];
- } else {
- if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
- idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
- if (isUndef(idxInOld)) { // New element
- nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- } else {
- elmToMove = oldCh[idxInOld];
- /* istanbul ignore if */
- if ("development" !== 'production' && !elmToMove) {
- warn(
- 'It seems there are duplicate keys that is causing an update error. ' +
- 'Make sure each v-for item has a unique key.'
- );
- }
- if (elmToMove.tag !== newStartVnode.tag) {
- // same key but different element. treat as new element
- nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- } else {
- patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
- oldCh[idxInOld] = undefined;
- canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
- newStartVnode = newCh[++newStartIdx];
- }
- }
- }
- }
- if (oldStartIdx > oldEndIdx) {
- before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
- addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
- } else if (newStartIdx > newEndIdx) {
- removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
- }
- }
-
- function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
- if (oldVnode === vnode) {
- return
- }
- // reuse element for static trees.
- // note we only do this if the vnode is cloned -
- // if the new node is not cloned it means the render functions have been
- // reset by the hot-reload-api and we need to do a proper re-render.
- if (vnode.isStatic &&
- oldVnode.isStatic &&
- vnode.key === oldVnode.key &&
- vnode.isCloned) {
- vnode.elm = oldVnode.elm;
- return
- }
- var i;
- var data = vnode.data;
- var hasData = isDef(data);
- if (hasData && isDef(i = data.hook) && isDef(i = i.prepatch)) {
- i(oldVnode, vnode);
- }
- var elm = vnode.elm = oldVnode.elm;
- var oldCh = oldVnode.children;
- var ch = vnode.children;
- if (hasData && isPatchable(vnode)) {
- for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
- if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
- }
- if (isUndef(vnode.text)) {
- if (isDef(oldCh) && isDef(ch)) {
- if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
- } else if (isDef(ch)) {
- if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
- addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
- } else if (isDef(oldCh)) {
- removeVnodes(elm, oldCh, 0, oldCh.length - 1);
- } else if (isDef(oldVnode.text)) {
- nodeOps.setTextContent(elm, '');
- }
- } else if (oldVnode.text !== vnode.text) {
- nodeOps.setTextContent(elm, vnode.text);
- }
- if (hasData) {
- if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
- }
- }
-
- function invokeInsertHook (vnode, queue, initial) {
- // delay insert hooks for component root nodes, invoke them after the
- // element is really inserted
- if (initial && vnode.parent) {
- vnode.parent.data.pendingInsert = queue;
- } else {
- for (var i = 0; i < queue.length; ++i) {
- queue[i].data.hook.insert(queue[i]);
- }
- }
- }
-
- var bailed = false;
- function hydrate (elm, vnode, insertedVnodeQueue) {
- {
- if (!assertNodeMatch(elm, vnode)) {
- return false
- }
- }
- vnode.elm = elm;
- var tag = vnode.tag;
- var data = vnode.data;
- var children = vnode.children;
- if (isDef(data)) {
- if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }
- if (isDef(i = vnode.child)) {
- // child component. it should have hydrated its own tree.
- initComponent(vnode, insertedVnodeQueue);
- return true
- }
- }
- if (isDef(tag)) {
- if (isDef(children)) {
- var childNodes = nodeOps.childNodes(elm);
- // empty element, allow client to pick up and populate children
- if (!childNodes.length) {
- createChildren(vnode, children, insertedVnodeQueue);
- } else {
- var childrenMatch = true;
- if (childNodes.length !== children.length) {
- childrenMatch = false;
- } else {
- for (var i$1 = 0; i$1 < children.length; i$1++) {
- if (!hydrate(childNodes[i$1], children[i$1], insertedVnodeQueue)) {
- childrenMatch = false;
- break
- }
- }
- }
- if (!childrenMatch) {
- if ("development" !== 'production' &&
- typeof console !== 'undefined' &&
- !bailed) {
- bailed = true;
- console.warn('Parent: ', elm);
- console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children);
- }
- return false
- }
- }
- }
- if (isDef(data)) {
- invokeCreateHooks(vnode, insertedVnodeQueue);
- }
- }
- return true
- }
-
- function assertNodeMatch (node, vnode) {
- if (vnode.tag) {
- return (
- vnode.tag.indexOf('vue-component') === 0 ||
- vnode.tag === nodeOps.tagName(node).toLowerCase()
- )
- } else {
- return _toString(vnode.text) === node.data
- }
- }
-
- return function patch (oldVnode, vnode, hydrating, removeOnly) {
- if (!vnode) {
- if (oldVnode) { invokeDestroyHook(oldVnode); }
- return
- }
-
- var elm, parent;
- var isInitialPatch = false;
- var insertedVnodeQueue = [];
-
- if (!oldVnode) {
- // empty mount, create new root element
- isInitialPatch = true;
- createElm(vnode, insertedVnodeQueue);
- } else {
- var isRealElement = isDef(oldVnode.nodeType);
- if (!isRealElement && sameVnode(oldVnode, vnode)) {
- patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
- } else {
- if (isRealElement) {
- // mounting to a real element
- // check if this is server-rendered content and if we can perform
- // a successful hydration.
- if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) {
- oldVnode.removeAttribute('server-rendered');
- hydrating = true;
- }
- if (hydrating) {
- if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
- invokeInsertHook(vnode, insertedVnodeQueue, true);
- return oldVnode
- } else {
- warn(
- 'The client-side rendered virtual DOM tree is not matching ' +
- 'server-rendered content. This is likely caused by incorrect ' +
- 'HTML markup, for example nesting block-level elements inside ' +
- '<p>, or missing <tbody>. Bailing hydration and performing ' +
- 'full client-side render.'
- );
- }
- }
- // either not server-rendered, or hydration failed.
- // create an empty node and replace it
- oldVnode = emptyNodeAt(oldVnode);
- }
- elm = oldVnode.elm;
- parent = nodeOps.parentNode(elm);
-
- createElm(vnode, insertedVnodeQueue);
-
- // component root element replaced.
- // update parent placeholder node element.
- if (vnode.parent) {
- vnode.parent.elm = vnode.elm;
- if (isPatchable(vnode)) {
- for (var i = 0; i < cbs.create.length; ++i) {
- cbs.create[i](emptyNode, vnode.parent);
- }
- }
- }
-
- if (parent !== null) {
- nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- } else if (isDef(oldVnode.tag)) {
- invokeDestroyHook(oldVnode);
- }
- }
- }
-
- invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
- return vnode.elm
- }
-}
-
-/* */
-
-var directives = {
- create: updateDirectives,
- update: updateDirectives,
- destroy: function unbindDirectives (vnode) {
- updateDirectives(vnode, emptyNode);
- }
-};
-
-function updateDirectives (
- oldVnode,
- vnode
-) {
- if (!oldVnode.data.directives && !vnode.data.directives) {
- return
- }
- var isCreate = oldVnode === emptyNode;
- var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
- var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
-
- var dirsWithInsert = [];
- var dirsWithPostpatch = [];
-
- var key, oldDir, dir;
- for (key in newDirs) {
- oldDir = oldDirs[key];
- dir = newDirs[key];
- if (!oldDir) {
- // new directive, bind
- callHook$1(dir, 'bind', vnode, oldVnode);
- if (dir.def && dir.def.inserted) {
- dirsWithInsert.push(dir);
- }
- } else {
- // existing directive, update
- dir.oldValue = oldDir.value;
- callHook$1(dir, 'update', vnode, oldVnode);
- if (dir.def && dir.def.componentUpdated) {
- dirsWithPostpatch.push(dir);
- }
- }
- }
-
- if (dirsWithInsert.length) {
- var callInsert = function () {
- dirsWithInsert.forEach(function (dir) {
- callHook$1(dir, 'inserted', vnode, oldVnode);
- });
- };
- if (isCreate) {
- mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert, 'dir-insert');
- } else {
- callInsert();
- }
- }
-
- if (dirsWithPostpatch.length) {
- mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', function () {
- dirsWithPostpatch.forEach(function (dir) {
- callHook$1(dir, 'componentUpdated', vnode, oldVnode);
- });
- }, 'dir-postpatch');
- }
-
- if (!isCreate) {
- for (key in oldDirs) {
- if (!newDirs[key]) {
- // no longer present, unbind
- callHook$1(oldDirs[key], 'unbind', oldVnode);
- }
- }
- }
-}
-
-var emptyModifiers = Object.create(null);
-
-function normalizeDirectives$1 (
- dirs,
- vm
-) {
- var res = Object.create(null);
- if (!dirs) {
- return res
- }
- var i, dir;
- for (i = 0; i < dirs.length; i++) {
- dir = dirs[i];
- if (!dir.modifiers) {
- dir.modifiers = emptyModifiers;
- }
- res[getRawDirName(dir)] = dir;
- dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
- }
- return res
-}
-
-function getRawDirName (dir) {
- return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
-}
-
-function callHook$1 (dir, hook, vnode, oldVnode) {
- var fn = dir.def && dir.def[hook];
- if (fn) {
- fn(vnode.elm, dir, vnode, oldVnode);
- }
-}
-
-var baseModules = [
- ref,
- directives
-];
-
-/* */
-
-function updateAttrs (oldVnode, vnode) {
- if (!oldVnode.data.attrs && !vnode.data.attrs) {
- return
- }
- var key, cur, old;
- var elm = vnode.elm;
- var oldAttrs = oldVnode.data.attrs || {};
- var attrs = vnode.data.attrs || {};
- // clone observed objects, as the user probably wants to mutate it
- if (attrs.__ob__) {
- attrs = vnode.data.attrs = extend({}, attrs);
- }
-
- for (key in attrs) {
- cur = attrs[key];
- old = oldAttrs[key];
- if (old !== cur) {
- setAttr(elm, key, cur);
- }
- }
- for (key in oldAttrs) {
- if (attrs[key] == null) {
- if (isXlink(key)) {
- elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
- } else if (!isEnumeratedAttr(key)) {
- elm.removeAttribute(key);
- }
- }
- }
-}
-
-function setAttr (el, key, value) {
- if (isBooleanAttr(key)) {
- // set attribute for blank value
- // e.g. <option disabled>Select one</option>
- if (isFalsyAttrValue(value)) {
- el.removeAttribute(key);
- } else {
- el.setAttribute(key, key);
- }
- } else if (isEnumeratedAttr(key)) {
- el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true');
- } else if (isXlink(key)) {
- if (isFalsyAttrValue(value)) {
- el.removeAttributeNS(xlinkNS, getXlinkProp(key));
- } else {
- el.setAttributeNS(xlinkNS, key, value);
- }
- } else {
- if (isFalsyAttrValue(value)) {
- el.removeAttribute(key);
- } else {
- el.setAttribute(key, value);
- }
- }
-}
-
-var attrs = {
- create: updateAttrs,
- update: updateAttrs
-};
-
-/* */
-
-function updateClass (oldVnode, vnode) {
- var el = vnode.elm;
- var data = vnode.data;
- var oldData = oldVnode.data;
- if (!data.staticClass && !data.class &&
- (!oldData || (!oldData.staticClass && !oldData.class))) {
- return
- }
-
- var cls = genClassForVnode(vnode);
-
- // handle transition classes
- var transitionClass = el._transitionClasses;
- if (transitionClass) {
- cls = concat(cls, stringifyClass(transitionClass));
- }
-
- // set the class
- if (cls !== el._prevClass) {
- el.setAttribute('class', cls);
- el._prevClass = cls;
- }
-}
-
-var klass = {
- create: updateClass,
- update: updateClass
-};
-
-// skip type checking this file because we need to attach private properties
-// to elements
-
-function updateDOMListeners (oldVnode, vnode) {
- if (!oldVnode.data.on && !vnode.data.on) {
- return
- }
- var on = vnode.data.on || {};
- var oldOn = oldVnode.data.on || {};
- var add = vnode.elm._v_add || (vnode.elm._v_add = function (event, handler, capture) {
- vnode.elm.addEventListener(event, handler, capture);
- });
- var remove = vnode.elm._v_remove || (vnode.elm._v_remove = function (event, handler) {
- vnode.elm.removeEventListener(event, handler);
- });
- updateListeners(on, oldOn, add, remove, vnode.context);
-}
-
-var events = {
- create: updateDOMListeners,
- update: updateDOMListeners
-};
-
-/* */
-
-function updateDOMProps (oldVnode, vnode) {
- if (!oldVnode.data.domProps && !vnode.data.domProps) {
- return
- }
- var key, cur;
- var elm = vnode.elm;
- var oldProps = oldVnode.data.domProps || {};
- var props = vnode.data.domProps || {};
- // clone observed objects, as the user probably wants to mutate it
- if (props.__ob__) {
- props = vnode.data.domProps = extend({}, props);
- }
-
- for (key in oldProps) {
- if (props[key] == null) {
- elm[key] = undefined;
- }
- }
- for (key in props) {
- // ignore children if the node has textContent or innerHTML,
- // as these will throw away existing DOM nodes and cause removal errors
- // on subsequent patches (#3360)
- if ((key === 'textContent' || key === 'innerHTML') && vnode.children) {
- vnode.children.length = 0;
- }
- cur = props[key];
- if (key === 'value') {
- // store value as _value as well since
- // non-string values will be stringified
- elm._value = cur;
- // avoid resetting cursor position when value is the same
- var strCur = cur == null ? '' : String(cur);
- if (elm.value !== strCur && !elm.composing) {
- elm.value = strCur;
- }
- } else {
- elm[key] = cur;
- }
- }
-}
-
-var domProps = {
- create: updateDOMProps,
- update: updateDOMProps
-};
-
-/* */
-
-var prefixes = ['Webkit', 'Moz', 'ms'];
-
-var testEl;
-var normalize = cached(function (prop) {
- testEl = testEl || document.createElement('div');
- prop = camelize(prop);
- if (prop !== 'filter' && (prop in testEl.style)) {
- return prop
- }
- var upper = prop.charAt(0).toUpperCase() + prop.slice(1);
- for (var i = 0; i < prefixes.length; i++) {
- var prefixed = prefixes[i] + upper;
- if (prefixed in testEl.style) {
- return prefixed
- }
- }
-});
-
-function updateStyle (oldVnode, vnode) {
- if ((!oldVnode.data || !oldVnode.data.style) && !vnode.data.style) {
- return
- }
- var cur, name;
- var el = vnode.elm;
- var oldStyle = oldVnode.data.style || {};
- var style = vnode.data.style || {};
-
- // handle string
- if (typeof style === 'string') {
- el.style.cssText = style;
- return
- }
-
- var needClone = style.__ob__;
-
- // handle array syntax
- if (Array.isArray(style)) {
- style = vnode.data.style = toObject(style);
- }
-
- // clone the style for future updates,
- // in case the user mutates the style object in-place.
- if (needClone) {
- style = vnode.data.style = extend({}, style);
- }
-
- for (name in oldStyle) {
- if (style[name] == null) {
- el.style[normalize(name)] = '';
- }
- }
- for (name in style) {
- cur = style[name];
- if (cur !== oldStyle[name]) {
- // ie9 setting to null has no effect, must use empty string
- el.style[normalize(name)] = cur == null ? '' : cur;
- }
- }
-}
-
-var style = {
- create: updateStyle,
- update: updateStyle
-};
-
-/* */
-
-/**
- * Add class with compatibility for SVG since classList is not supported on
- * SVG elements in IE
- */
-function addClass (el, cls) {
- /* istanbul ignore else */
- if (el.classList) {
- if (cls.indexOf(' ') > -1) {
- cls.split(/\s+/).forEach(function (c) { return el.classList.add(c); });
- } else {
- el.classList.add(cls);
- }
- } else {
- var cur = ' ' + el.getAttribute('class') + ' ';
- if (cur.indexOf(' ' + cls + ' ') < 0) {
- el.setAttribute('class', (cur + cls).trim());
- }
- }
-}
-
-/**
- * Remove class with compatibility for SVG since classList is not supported on
- * SVG elements in IE
- */
-function removeClass (el, cls) {
- /* istanbul ignore else */
- if (el.classList) {
- if (cls.indexOf(' ') > -1) {
- cls.split(/\s+/).forEach(function (c) { return el.classList.remove(c); });
- } else {
- el.classList.remove(cls);
- }
- } else {
- var cur = ' ' + el.getAttribute('class') + ' ';
- var tar = ' ' + cls + ' ';
- while (cur.indexOf(tar) >= 0) {
- cur = cur.replace(tar, ' ');
- }
- el.setAttribute('class', cur.trim());
- }
-}
-
-/* */
-
-var hasTransition = inBrowser && !isIE9;
-var TRANSITION = 'transition';
-var ANIMATION = 'animation';
-
-// Transition property/event sniffing
-var transitionProp = 'transition';
-var transitionEndEvent = 'transitionend';
-var animationProp = 'animation';
-var animationEndEvent = 'animationend';
-if (hasTransition) {
- /* istanbul ignore if */
- if (window.ontransitionend === undefined &&
- window.onwebkittransitionend !== undefined) {
- transitionProp = 'WebkitTransition';
- transitionEndEvent = 'webkitTransitionEnd';
- }
- if (window.onanimationend === undefined &&
- window.onwebkitanimationend !== undefined) {
- animationProp = 'WebkitAnimation';
- animationEndEvent = 'webkitAnimationEnd';
- }
-}
-
-var raf = (inBrowser && window.requestAnimationFrame) || setTimeout;
-function nextFrame (fn) {
- raf(function () {
- raf(fn);
- });
-}
-
-function addTransitionClass (el, cls) {
- (el._transitionClasses || (el._transitionClasses = [])).push(cls);
- addClass(el, cls);
-}
-
-function removeTransitionClass (el, cls) {
- if (el._transitionClasses) {
- remove$1(el._transitionClasses, cls);
- }
- removeClass(el, cls);
-}
-
-function whenTransitionEnds (
- el,
- expectedType,
- cb
-) {
- var ref = getTransitionInfo(el, expectedType);
- var type = ref.type;
- var timeout = ref.timeout;
- var propCount = ref.propCount;
- if (!type) { return cb() }
- var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
- var ended = 0;
- var end = function () {
- el.removeEventListener(event, onEnd);
- cb();
- };
- var onEnd = function (e) {
- if (e.target === el) {
- if (++ended >= propCount) {
- end();
- }
- }
- };
- setTimeout(function () {
- if (ended < propCount) {
- end();
- }
- }, timeout + 1);
- el.addEventListener(event, onEnd);
-}
-
-var transformRE = /\b(transform|all)(,|$)/;
-
-function getTransitionInfo (el, expectedType) {
- var styles = window.getComputedStyle(el);
- var transitioneDelays = styles[transitionProp + 'Delay'].split(', ');
- var transitionDurations = styles[transitionProp + 'Duration'].split(', ');
- var transitionTimeout = getTimeout(transitioneDelays, transitionDurations);
- var animationDelays = styles[animationProp + 'Delay'].split(', ');
- var animationDurations = styles[animationProp + 'Duration'].split(', ');
- var animationTimeout = getTimeout(animationDelays, animationDurations);
-
- var type;
- var timeout = 0;
- var propCount = 0;
- /* istanbul ignore if */
- if (expectedType === TRANSITION) {
- if (transitionTimeout > 0) {
- type = TRANSITION;
- timeout = transitionTimeout;
- propCount = transitionDurations.length;
- }
- } else if (expectedType === ANIMATION) {
- if (animationTimeout > 0) {
- type = ANIMATION;
- timeout = animationTimeout;
- propCount = animationDurations.length;
- }
- } else {
- timeout = Math.max(transitionTimeout, animationTimeout);
- type = timeout > 0
- ? transitionTimeout > animationTimeout
- ? TRANSITION
- : ANIMATION
- : null;
- propCount = type
- ? type === TRANSITION
- ? transitionDurations.length
- : animationDurations.length
- : 0;
- }
- var hasTransform =
- type === TRANSITION &&
- transformRE.test(styles[transitionProp + 'Property']);
- return {
- type: type,
- timeout: timeout,
- propCount: propCount,
- hasTransform: hasTransform
- }
-}
-
-function getTimeout (delays, durations) {
- return Math.max.apply(null, durations.map(function (d, i) {
- return toMs(d) + toMs(delays[i])
- }))
-}
-
-function toMs (s) {
- return Number(s.slice(0, -1)) * 1000
-}
-
-/* */
-
-function enter (vnode) {
- var el = vnode.elm;
-
- // call leave callback now
- if (el._leaveCb) {
- el._leaveCb.cancelled = true;
- el._leaveCb();
- }
-
- var data = resolveTransition(vnode.data.transition);
- if (!data) {
- return
- }
-
- /* istanbul ignore if */
- if (el._enterCb || el.nodeType !== 1) {
- return
- }
-
- var css = data.css;
- var type = data.type;
- var enterClass = data.enterClass;
- var enterActiveClass = data.enterActiveClass;
- var appearClass = data.appearClass;
- var appearActiveClass = data.appearActiveClass;
- var beforeEnter = data.beforeEnter;
- var enter = data.enter;
- var afterEnter = data.afterEnter;
- var enterCancelled = data.enterCancelled;
- var beforeAppear = data.beforeAppear;
- var appear = data.appear;
- var afterAppear = data.afterAppear;
- var appearCancelled = data.appearCancelled;
-
- // activeInstance will always be the <transition> component managing this
- // transition. One edge case to check is when the <transition> is placed
- // as the root node of a child component. In that case we need to check
- // <transition>'s parent for appear check.
- var transitionNode = activeInstance.$vnode;
- var context = transitionNode && transitionNode.parent
- ? transitionNode.parent.context
- : activeInstance;
-
- var isAppear = !context._isMounted || !vnode.isRootInsert;
-
- if (isAppear && !appear && appear !== '') {
- return
- }
-
- var startClass = isAppear ? appearClass : enterClass;
- var activeClass = isAppear ? appearActiveClass : enterActiveClass;
- var beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter;
- var enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter;
- var afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter;
- var enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled;
-
- var expectsCSS = css !== false && !isIE9;
- var userWantsControl =
- enterHook &&
- // enterHook may be a bound method which exposes
- // the length of original fn as _length
- (enterHook._length || enterHook.length) > 1;
-
- var cb = el._enterCb = once(function () {
- if (expectsCSS) {
- removeTransitionClass(el, activeClass);
- }
- if (cb.cancelled) {
- if (expectsCSS) {
- removeTransitionClass(el, startClass);
- }
- enterCancelledHook && enterCancelledHook(el);
- } else {
- afterEnterHook && afterEnterHook(el);
- }
- el._enterCb = null;
- });
-
- if (!vnode.data.show) {
- // remove pending leave element on enter by injecting an insert hook
- mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', function () {
- var parent = el.parentNode;
- var pendingNode = parent && parent._pending && parent._pending[vnode.key];
- if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) {
- pendingNode.elm._leaveCb();
- }
- enterHook && enterHook(el, cb);
- }, 'transition-insert');
- }
-
- // start enter transition
- beforeEnterHook && beforeEnterHook(el);
- if (expectsCSS) {
- addTransitionClass(el, startClass);
- addTransitionClass(el, activeClass);
- nextFrame(function () {
- removeTransitionClass(el, startClass);
- if (!cb.cancelled && !userWantsControl) {
- whenTransitionEnds(el, type, cb);
- }
- });
- }
-
- if (vnode.data.show) {
- enterHook && enterHook(el, cb);
- }
-
- if (!expectsCSS && !userWantsControl) {
- cb();
- }
-}
-
-function leave (vnode, rm) {
- var el = vnode.elm;
-
- // call enter callback now
- if (el._enterCb) {
- el._enterCb.cancelled = true;
- el._enterCb();
- }
-
- var data = resolveTransition(vnode.data.transition);
- if (!data) {
- return rm()
- }
-
- /* istanbul ignore if */
- if (el._leaveCb || el.nodeType !== 1) {
- return
- }
-
- var css = data.css;
- var type = data.type;
- var leaveClass = data.leaveClass;
- var leaveActiveClass = data.leaveActiveClass;
- var beforeLeave = data.beforeLeave;
- var leave = data.leave;
- var afterLeave = data.afterLeave;
- var leaveCancelled = data.leaveCancelled;
- var delayLeave = data.delayLeave;
-
- var expectsCSS = css !== false && !isIE9;
- var userWantsControl =
- leave &&
- // leave hook may be a bound method which exposes
- // the length of original fn as _length
- (leave._length || leave.length) > 1;
-
- var cb = el._leaveCb = once(function () {
- if (el.parentNode && el.parentNode._pending) {
- el.parentNode._pending[vnode.key] = null;
- }
- if (expectsCSS) {
- removeTransitionClass(el, leaveActiveClass);
- }
- if (cb.cancelled) {
- if (expectsCSS) {
- removeTransitionClass(el, leaveClass);
- }
- leaveCancelled && leaveCancelled(el);
- } else {
- rm();
- afterLeave && afterLeave(el);
- }
- el._leaveCb = null;
- });
-
- if (delayLeave) {
- delayLeave(performLeave);
- } else {
- performLeave();
- }
-
- function performLeave () {
- // the delayed leave may have already been cancelled
- if (cb.cancelled) {
- return
- }
- // record leaving element
- if (!vnode.data.show) {
- (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode;
- }
- beforeLeave && beforeLeave(el);
- if (expectsCSS) {
- addTransitionClass(el, leaveClass);
- addTransitionClass(el, leaveActiveClass);
- nextFrame(function () {
- removeTransitionClass(el, leaveClass);
- if (!cb.cancelled && !userWantsControl) {
- whenTransitionEnds(el, type, cb);
- }
- });
- }
- leave && leave(el, cb);
- if (!expectsCSS && !userWantsControl) {
- cb();
- }
- }
-}
-
-function resolveTransition (def$$1) {
- if (!def$$1) {
- return
- }
- /* istanbul ignore else */
- if (typeof def$$1 === 'object') {
- var res = {};
- if (def$$1.css !== false) {
- extend(res, autoCssTransition(def$$1.name || 'v'));
- }
- extend(res, def$$1);
- return res
- } else if (typeof def$$1 === 'string') {
- return autoCssTransition(def$$1)
- }
-}
-
-var autoCssTransition = cached(function (name) {
- return {
- enterClass: (name + "-enter"),
- leaveClass: (name + "-leave"),
- appearClass: (name + "-enter"),
- enterActiveClass: (name + "-enter-active"),
- leaveActiveClass: (name + "-leave-active"),
- appearActiveClass: (name + "-enter-active")
- }
-});
-
-function once (fn) {
- var called = false;
- return function () {
- if (!called) {
- called = true;
- fn();
- }
- }
-}
-
-var transition = inBrowser ? {
- create: function create (_, vnode) {
- if (!vnode.data.show) {
- enter(vnode);
- }
- },
- remove: function remove (vnode, rm) {
- /* istanbul ignore else */
- if (!vnode.data.show) {
- leave(vnode, rm);
- } else {
- rm();
- }
- }
-} : {};
-
-var platformModules = [
- attrs,
- klass,
- events,
- domProps,
- style,
- transition
-];
-
-/* */
-
-// the directive module should be applied last, after all
-// built-in modules have been applied.
-var modules = platformModules.concat(baseModules);
-
-var patch$1 = createPatchFunction({ nodeOps: nodeOps, modules: modules });
-
-/**
- * Not type checking this file because flow doesn't like attaching
- * properties to Elements.
- */
-
-var modelableTagRE = /^input|select|textarea|vue-component-[0-9]+(-[0-9a-zA-Z_\-]*)?$/;
-
-/* istanbul ignore if */
-if (isIE9) {
- // http://www.matts411.com/post/internet-explorer-9-oninput/
- document.addEventListener('selectionchange', function () {
- var el = document.activeElement;
- if (el && el.vmodel) {
- trigger(el, 'input');
- }
- });
-}
-
-var model = {
- inserted: function inserted (el, binding, vnode) {
- {
- if (!modelableTagRE.test(vnode.tag)) {
- warn(
- "v-model is not supported on element type: <" + (vnode.tag) + ">. " +
- 'If you are working with contenteditable, it\'s recommended to ' +
- 'wrap a library dedicated for that purpose inside a custom component.',
- vnode.context
- );
- }
- }
- if (vnode.tag === 'select') {
- var cb = function () {
- setSelected(el, binding, vnode.context);
- };
- cb();
- /* istanbul ignore if */
- if (isIE || isEdge) {
- setTimeout(cb, 0);
- }
- } else if (
- (vnode.tag === 'textarea' || el.type === 'text') &&
- !binding.modifiers.lazy
- ) {
- if (!isAndroid) {
- el.addEventListener('compositionstart', onCompositionStart);
- el.addEventListener('compositionend', onCompositionEnd);
- }
- /* istanbul ignore if */
- if (isIE9) {
- el.vmodel = true;
- }
- }
- },
- componentUpdated: function componentUpdated (el, binding, vnode) {
- if (vnode.tag === 'select') {
- setSelected(el, binding, vnode.context);
- // in case the options rendered by v-for have changed,
- // it's possible that the value is out-of-sync with the rendered options.
- // detect such cases and filter out values that no longer has a matchig
- // option in the DOM.
- var needReset = el.multiple
- ? binding.value.some(function (v) { return hasNoMatchingOption(v, el.options); })
- : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, el.options);
- if (needReset) {
- trigger(el, 'change');
- }
- }
- }
-};
-
-function setSelected (el, binding, vm) {
- var value = binding.value;
- var isMultiple = el.multiple;
- if (isMultiple && !Array.isArray(value)) {
- "development" !== 'production' && warn(
- "<select multiple v-model=\"" + (binding.expression) + "\"> " +
- "expects an Array value for its binding, but got " + (Object.prototype.toString.call(value).slice(8, -1)),
- vm
- );
- return
- }
- var selected, option;
- for (var i = 0, l = el.options.length; i < l; i++) {
- option = el.options[i];
- if (isMultiple) {
- selected = looseIndexOf(value, getValue(option)) > -1;
- if (option.selected !== selected) {
- option.selected = selected;
- }
- } else {
- if (looseEqual(getValue(option), value)) {
- if (el.selectedIndex !== i) {
- el.selectedIndex = i;
- }
- return
- }
- }
- }
- if (!isMultiple) {
- el.selectedIndex = -1;
- }
-}
-
-function hasNoMatchingOption (value, options) {
- for (var i = 0, l = options.length; i < l; i++) {
- if (looseEqual(getValue(options[i]), value)) {
- return false
- }
- }
- return true
-}
-
-function getValue (option) {
- return '_value' in option
- ? option._value
- : option.value
-}
-
-function onCompositionStart (e) {
- e.target.composing = true;
-}
-
-function onCompositionEnd (e) {
- e.target.composing = false;
- trigger(e.target, 'input');
-}
-
-function trigger (el, type) {
- var e = document.createEvent('HTMLEvents');
- e.initEvent(type, true, true);
- el.dispatchEvent(e);
-}
-
-/* */
-
-// recursively search for possible transition defined inside the component root
-function locateNode (vnode) {
- return vnode.child && (!vnode.data || !vnode.data.transition)
- ? locateNode(vnode.child._vnode)
- : vnode
-}
-
-var show = {
- bind: function bind (el, ref, vnode) {
- var value = ref.value;
-
- vnode = locateNode(vnode);
- var transition = vnode.data && vnode.data.transition;
- if (value && transition && !isIE9) {
- enter(vnode);
- }
- var originalDisplay = el.style.display === 'none' ? '' : el.style.display;
- el.style.display = value ? originalDisplay : 'none';
- el.__vOriginalDisplay = originalDisplay;
- },
- update: function update (el, ref, vnode) {
- var value = ref.value;
- var oldValue = ref.oldValue;
-
- /* istanbul ignore if */
- if (value === oldValue) { return }
- vnode = locateNode(vnode);
- var transition = vnode.data && vnode.data.transition;
- if (transition && !isIE9) {
- if (value) {
- enter(vnode);
- el.style.display = el.__vOriginalDisplay;
- } else {
- leave(vnode, function () {
- el.style.display = 'none';
- });
- }
- } else {
- el.style.display = value ? el.__vOriginalDisplay : 'none';
- }
- }
-};
-
-var platformDirectives = {
- model: model,
- show: show
-};
-
-/* */
-
-// Provides transition support for a single element/component.
-// supports transition mode (out-in / in-out)
-
-var transitionProps = {
- name: String,
- appear: Boolean,
- css: Boolean,
- mode: String,
- type: String,
- enterClass: String,
- leaveClass: String,
- enterActiveClass: String,
- leaveActiveClass: String,
- appearClass: String,
- appearActiveClass: String
-};
-
-// in case the child is also an abstract component, e.g. <keep-alive>
-// we want to recrusively retrieve the real component to be rendered
-function getRealChild (vnode) {
- var compOptions = vnode && vnode.componentOptions;
- if (compOptions && compOptions.Ctor.options.abstract) {
- return getRealChild(getFirstComponentChild(compOptions.children))
- } else {
- return vnode
- }
-}
-
-function extractTransitionData (comp) {
- var data = {};
- var options = comp.$options;
- // props
- for (var key in options.propsData) {
- data[key] = comp[key];
- }
- // events.
- // extract listeners and pass them directly to the transition methods
- var listeners = options._parentListeners;
- for (var key$1 in listeners) {
- data[camelize(key$1)] = listeners[key$1].fn;
- }
- return data
-}
-
-function placeholder (h, rawChild) {
- return /\d-keep-alive$/.test(rawChild.tag)
- ? h('keep-alive')
- : null
-}
-
-function hasParentTransition (vnode) {
- while ((vnode = vnode.parent)) {
- if (vnode.data.transition) {
- return true
- }
- }
-}
-
-var Transition = {
- name: 'transition',
- props: transitionProps,
- abstract: true,
- render: function render (h) {
- var this$1 = this;
-
- var children = this.$slots.default;
- if (!children) {
- return
- }
-
- // filter out text nodes (possible whitespaces)
- children = children.filter(function (c) { return c.tag; });
- /* istanbul ignore if */
- if (!children.length) {
- return
- }
-
- // warn multiple elements
- if ("development" !== 'production' && children.length > 1) {
- warn(
- '<transition> can only be used on a single element. Use ' +
- '<transition-group> for lists.',
- this.$parent
- );
- }
-
- var mode = this.mode;
-
- // warn invalid mode
- if ("development" !== 'production' &&
- mode && mode !== 'in-out' && mode !== 'out-in') {
- warn(
- 'invalid <transition> mode: ' + mode,
- this.$parent
- );
- }
-
- var rawChild = children[0];
-
- // if this is a component root node and the component's
- // parent container node also has transition, skip.
- if (hasParentTransition(this.$vnode)) {
- return rawChild
- }
-
- // apply transition data to child
- // use getRealChild() to ignore abstract components e.g. keep-alive
- var child = getRealChild(rawChild);
- /* istanbul ignore if */
- if (!child) {
- return rawChild
- }
-
- if (this._leaving) {
- return placeholder(h, rawChild)
- }
-
- var key = child.key = child.key == null || child.isStatic
- ? ("__v" + (child.tag + this._uid) + "__")
- : child.key;
- var data = (child.data || (child.data = {})).transition = extractTransitionData(this);
- var oldRawChild = this._vnode;
- var oldChild = getRealChild(oldRawChild);
-
- // mark v-show
- // so that the transition module can hand over the control to the directive
- if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) {
- child.data.show = true;
- }
-
- if (oldChild && oldChild.data && oldChild.key !== key) {
- // replace old child transition data with fresh one
- // important for dynamic transitions!
- var oldData = oldChild.data.transition = extend({}, data);
-
- // handle transition mode
- if (mode === 'out-in') {
- // return placeholder node and queue update when leave finishes
- this._leaving = true;
- mergeVNodeHook(oldData, 'afterLeave', function () {
- this$1._leaving = false;
- this$1.$forceUpdate();
- }, key);
- return placeholder(h, rawChild)
- } else if (mode === 'in-out') {
- var delayedLeave;
- var performLeave = function () { delayedLeave(); };
- mergeVNodeHook(data, 'afterEnter', performLeave, key);
- mergeVNodeHook(data, 'enterCancelled', performLeave, key);
- mergeVNodeHook(oldData, 'delayLeave', function (leave) {
- delayedLeave = leave;
- }, key);
- }
- }
-
- return rawChild
- }
-};
-
-/* */
-
-// Provides transition support for list items.
-// supports move transitions using the FLIP technique.
-
-// Because the vdom's children update algorithm is "unstable" - i.e.
-// it doesn't guarantee the relative positioning of removed elements,
-// we force transition-group to update its children into two passes:
-// in the first pass, we remove all nodes that need to be removed,
-// triggering their leaving transition; in the second pass, we insert/move
-// into the final disired state. This way in the second pass removed
-// nodes will remain where they should be.
-
-var props = extend({
- tag: String,
- moveClass: String
-}, transitionProps);
-
-delete props.mode;
-
-var TransitionGroup = {
- props: props,
-
- render: function render (h) {
- var tag = this.tag || this.$vnode.data.tag || 'span';
- var map = Object.create(null);
- var prevChildren = this.prevChildren = this.children;
- var rawChildren = this.$slots.default || [];
- var children = this.children = [];
- var transitionData = extractTransitionData(this);
-
- for (var i = 0; i < rawChildren.length; i++) {
- var c = rawChildren[i];
- if (c.tag) {
- if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
- children.push(c);
- map[c.key] = c
- ;(c.data || (c.data = {})).transition = transitionData;
- } else {
- var opts = c.componentOptions;
- var name = opts
- ? (opts.Ctor.options.name || opts.tag)
- : c.tag;
- warn(("<transition-group> children must be keyed: <" + name + ">"));
- }
- }
- }
-
- if (prevChildren) {
- var kept = [];
- var removed = [];
- for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {
- var c$1 = prevChildren[i$1];
- c$1.data.transition = transitionData;
- c$1.data.pos = c$1.elm.getBoundingClientRect();
- if (map[c$1.key]) {
- kept.push(c$1);
- } else {
- removed.push(c$1);
- }
- }
- this.kept = h(tag, null, kept);
- this.removed = removed;
- }
-
- return h(tag, null, children)
- },
-
- beforeUpdate: function beforeUpdate () {
- // force removing pass
- this.__patch__(
- this._vnode,
- this.kept,
- false, // hydrating
- true // removeOnly (!important, avoids unnecessary moves)
- );
- this._vnode = this.kept;
- },
-
- updated: function updated () {
- var children = this.prevChildren;
- var moveClass = this.moveClass || (this.name + '-move');
- if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
- return
- }
-
- // we divide the work into three loops to avoid mixing DOM reads and writes
- // in each iteration - which helps prevent layout thrashing.
- children.forEach(callPendingCbs);
- children.forEach(recordPosition);
- children.forEach(applyTranslation);
-
- // force reflow to put everything in position
- var f = document.body.offsetHeight; // eslint-disable-line
-
- children.forEach(function (c) {
- if (c.data.moved) {
- var el = c.elm;
- var s = el.style;
- addTransitionClass(el, moveClass);
- s.transform = s.WebkitTransform = s.transitionDuration = '';
- el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
- if (!e || /transform$/.test(e.propertyName)) {
- el.removeEventListener(transitionEndEvent, cb);
- el._moveCb = null;
- removeTransitionClass(el, moveClass);
- }
- });
- }
- });
- },
-
- methods: {
- hasMove: function hasMove (el, moveClass) {
- /* istanbul ignore if */
- if (!hasTransition) {
- return false
- }
- if (this._hasMove != null) {
- return this._hasMove
- }
- addTransitionClass(el, moveClass);
- var info = getTransitionInfo(el);
- removeTransitionClass(el, moveClass);
- return (this._hasMove = info.hasTransform)
- }
- }
-};
-
-function callPendingCbs (c) {
- /* istanbul ignore if */
- if (c.elm._moveCb) {
- c.elm._moveCb();
- }
- /* istanbul ignore if */
- if (c.elm._enterCb) {
- c.elm._enterCb();
- }
-}
-
-function recordPosition (c) {
- c.data.newPos = c.elm.getBoundingClientRect();
-}
-
-function applyTranslation (c) {
- var oldPos = c.data.pos;
- var newPos = c.data.newPos;
- var dx = oldPos.left - newPos.left;
- var dy = oldPos.top - newPos.top;
- if (dx || dy) {
- c.data.moved = true;
- var s = c.elm.style;
- s.transform = s.WebkitTransform = "translate(" + dx + "px," + dy + "px)";
- s.transitionDuration = '0s';
- }
-}
-
-var platformComponents = {
- Transition: Transition,
- TransitionGroup: TransitionGroup
-};
-
-/* */
-
-// install platform specific utils
-Vue$3.config.isUnknownElement = isUnknownElement;
-Vue$3.config.isReservedTag = isReservedTag;
-Vue$3.config.getTagNamespace = getTagNamespace;
-Vue$3.config.mustUseProp = mustUseProp;
-
-// install platform runtime directives & components
-extend(Vue$3.options.directives, platformDirectives);
-extend(Vue$3.options.components, platformComponents);
-
-// install platform patch function
-Vue$3.prototype.__patch__ = config._isServer ? noop : patch$1;
-
-// wrap mount
-Vue$3.prototype.$mount = function (
- el,
- hydrating
-) {
- el = el && !config._isServer ? query(el) : undefined;
- return this._mount(el, hydrating)
-};
-
-// devtools global hook
-/* istanbul ignore next */
-setTimeout(function () {
- if (config.devtools) {
- if (devtools) {
- devtools.emit('init', Vue$3);
- } else if (
- "development" !== 'production' &&
- inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)
- ) {
- console.log(
- 'Download the Vue Devtools for a better development experience:\n' +
- 'https://github.com/vuejs/vue-devtools'
- );
- }
- }
-}, 0);
-
-/* */
-
-// check whether current browser encodes a char inside attribute values
-function shouldDecode (content, encoded) {
- var div = document.createElement('div');
- div.innerHTML = "<div a=\"" + content + "\">";
- return div.innerHTML.indexOf(encoded) > 0
-}
-
-// #3663
-// IE encodes newlines inside attribute values while other browsers don't
-var shouldDecodeNewlines = inBrowser ? shouldDecode('\n', '&#10;') : false;
-
-/* */
-
-var decoder = document.createElement('div');
-
-function decode (html) {
- decoder.innerHTML = html;
- return decoder.textContent
-}
-
-/**
- * Not type-checking this file because it's mostly vendor code.
- */
-
-/*!
- * HTML Parser By John Resig (ejohn.org)
- * Modified by Juriy "kangax" Zaytsev
- * Original code by Erik Arvidsson, Mozilla Public License
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
- */
-
-// Regular Expressions for parsing tags and attributes
-var singleAttrIdentifier = /([^\s"'<>\/=]+)/;
-var singleAttrAssign = /(?:=)/;
-var singleAttrValues = [
- // attr value double quotes
- /"([^"]*)"+/.source,
- // attr value, single quotes
- /'([^']*)'+/.source,
- // attr value, no quotes
- /([^\s"'=<>`]+)/.source
-];
-var attribute = new RegExp(
- '^\\s*' + singleAttrIdentifier.source +
- '(?:\\s*(' + singleAttrAssign.source + ')' +
- '\\s*(?:' + singleAttrValues.join('|') + '))?'
-);
-
-// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
-// but for Vue templates we can enforce a simple charset
-var ncname = '[a-zA-Z_][\\w\\-\\.]*';
-var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
-var startTagOpen = new RegExp('^<' + qnameCapture);
-var startTagClose = /^\s*(\/?)>/;
-var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
-var doctype = /^<!DOCTYPE [^>]+>/i;
-
-var IS_REGEX_CAPTURING_BROKEN = false;
-'x'.replace(/x(.)?/g, function (m, g) {
- IS_REGEX_CAPTURING_BROKEN = g === '';
-});
-
-// Special Elements (can contain anything)
-var isSpecialTag = makeMap('script,style', true);
-
-var reCache = {};
-
-var ltRE = /&lt;/g;
-var gtRE = /&gt;/g;
-var nlRE = /&#10;/g;
-var ampRE = /&amp;/g;
-var quoteRE = /&quot;/g;
-
-function decodeAttr (value, shouldDecodeNewlines) {
- if (shouldDecodeNewlines) {
- value = value.replace(nlRE, '\n');
- }
- return value
- .replace(ltRE, '<')
- .replace(gtRE, '>')
- .replace(ampRE, '&')
- .replace(quoteRE, '"')
-}
-
-function parseHTML (html, options) {
- var stack = [];
- var expectHTML = options.expectHTML;
- var isUnaryTag$$1 = options.isUnaryTag || no;
- var index = 0;
- var last, lastTag;
- while (html) {
- last = html;
- // Make sure we're not in a script or style element
- if (!lastTag || !isSpecialTag(lastTag)) {
- var textEnd = html.indexOf('<');
- if (textEnd === 0) {
- // Comment:
- if (/^<!--/.test(html)) {
- var commentEnd = html.indexOf('-->');
-
- if (commentEnd >= 0) {
- advance(commentEnd + 3);
- continue
- }
- }
-
- // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
- if (/^<!\[/.test(html)) {
- var conditionalEnd = html.indexOf(']>');
-
- if (conditionalEnd >= 0) {
- advance(conditionalEnd + 2);
- continue
- }
- }
-
- // Doctype:
- var doctypeMatch = html.match(doctype);
- if (doctypeMatch) {
- advance(doctypeMatch[0].length);
- continue
- }
-
- // End tag:
- var endTagMatch = html.match(endTag);
- if (endTagMatch) {
- var curIndex = index;
- advance(endTagMatch[0].length);
- parseEndTag(endTagMatch[0], endTagMatch[1], curIndex, index);
- continue
- }
-
- // Start tag:
- var startTagMatch = parseStartTag();
- if (startTagMatch) {
- handleStartTag(startTagMatch);
- continue
- }
- }
-
- var text = void 0;
- if (textEnd >= 0) {
- text = html.substring(0, textEnd);
- advance(textEnd);
- } else {
- text = html;
- html = '';
- }
-
- if (options.chars) {
- options.chars(text);
- }
- } else {
- var stackedTag = lastTag.toLowerCase();
- var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
- var endTagLength = 0;
- var rest = html.replace(reStackedTag, function (all, text, endTag) {
- endTagLength = endTag.length;
- if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
- text = text
- .replace(/<!--([\s\S]*?)-->/g, '$1')
- .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
- }
- if (options.chars) {
- options.chars(text);
- }
- return ''
- });
- index += html.length - rest.length;
- html = rest;
- parseEndTag('</' + stackedTag + '>', stackedTag, index - endTagLength, index);
- }
-
- if (html === last) {
- throw new Error('Error parsing template:\n\n' + html)
- }
- }
-
- // Clean up any remaining tags
- parseEndTag();
-
- function advance (n) {
- index += n;
- html = html.substring(n);
- }
-
- function parseStartTag () {
- var start = html.match(startTagOpen);
- if (start) {
- var match = {
- tagName: start[1],
- attrs: [],
- start: index
- };
- advance(start[0].length);
- var end, attr;
- while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
- advance(attr[0].length);
- match.attrs.push(attr);
- }
- if (end) {
- match.unarySlash = end[1];
- advance(end[0].length);
- match.end = index;
- return match
- }
- }
- }
-
- function handleStartTag (match) {
- var tagName = match.tagName;
- var unarySlash = match.unarySlash;
-
- if (expectHTML) {
- if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
- parseEndTag('', lastTag);
- }
- if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
- parseEndTag('', tagName);
- }
- }
-
- var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;
-
- var l = match.attrs.length;
- var attrs = new Array(l);
- for (var i = 0; i < l; i++) {
- var args = match.attrs[i];
- // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
- if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
- if (args[3] === '') { delete args[3]; }
- if (args[4] === '') { delete args[4]; }
- if (args[5] === '') { delete args[5]; }
- }
- var value = args[3] || args[4] || args[5] || '';
- attrs[i] = {
- name: args[1],
- value: decodeAttr(
- value,
- options.shouldDecodeNewlines
- )
- };
- }
-
- if (!unary) {
- stack.push({ tag: tagName, attrs: attrs });
- lastTag = tagName;
- unarySlash = '';
- }
-
- if (options.start) {
- options.start(tagName, attrs, unary, match.start, match.end);
- }
- }
-
- function parseEndTag (tag, tagName, start, end) {
- var pos;
- if (start == null) { start = index; }
- if (end == null) { end = index; }
-
- // Find the closest opened tag of the same type
- if (tagName) {
- var needle = tagName.toLowerCase();
- for (pos = stack.length - 1; pos >= 0; pos--) {
- if (stack[pos].tag.toLowerCase() === needle) {
- break
- }
- }
- } else {
- // If no tag name is provided, clean shop
- pos = 0;
- }
-
- if (pos >= 0) {
- // Close all the open elements, up the stack
- for (var i = stack.length - 1; i >= pos; i--) {
- if (options.end) {
- options.end(stack[i].tag, start, end);
- }
- }
-
- // Remove the open elements from the stack
- stack.length = pos;
- lastTag = pos && stack[pos - 1].tag;
- } else if (tagName.toLowerCase() === 'br') {
- if (options.start) {
- options.start(tagName, [], true, start, end);
- }
- } else if (tagName.toLowerCase() === 'p') {
- if (options.start) {
- options.start(tagName, [], false, start, end);
- }
- if (options.end) {
- options.end(tagName, start, end);
- }
- }
- }
-}
-
-/* */
-
-function parseFilters (exp) {
- var inSingle = false;
- var inDouble = false;
- var curly = 0;
- var square = 0;
- var paren = 0;
- var lastFilterIndex = 0;
- var c, prev, i, expression, filters;
-
- for (i = 0; i < exp.length; i++) {
- prev = c;
- c = exp.charCodeAt(i);
- if (inSingle) {
- // check single quote
- if (c === 0x27 && prev !== 0x5C) { inSingle = !inSingle; }
- } else if (inDouble) {
- // check double quote
- if (c === 0x22 && prev !== 0x5C) { inDouble = !inDouble; }
- } else if (
- c === 0x7C && // pipe
- exp.charCodeAt(i + 1) !== 0x7C &&
- exp.charCodeAt(i - 1) !== 0x7C &&
- !curly && !square && !paren
- ) {
- if (expression === undefined) {
- // first filter, end of expression
- lastFilterIndex = i + 1;
- expression = exp.slice(0, i).trim();
- } else {
- pushFilter();
- }
- } else {
- switch (c) {
- case 0x22: inDouble = true; break // "
- case 0x27: inSingle = true; break // '
- case 0x28: paren++; break // (
- case 0x29: paren--; break // )
- case 0x5B: square++; break // [
- case 0x5D: square--; break // ]
- case 0x7B: curly++; break // {
- case 0x7D: curly--; break // }
- }
- }
- }
-
- if (expression === undefined) {
- expression = exp.slice(0, i).trim();
- } else if (lastFilterIndex !== 0) {
- pushFilter();
- }
-
- function pushFilter () {
- (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
- lastFilterIndex = i + 1;
- }
-
- if (filters) {
- for (i = 0; i < filters.length; i++) {
- expression = wrapFilter(expression, filters[i]);
- }
- }
-
- return expression
-}
-
-function wrapFilter (exp, filter) {
- var i = filter.indexOf('(');
- if (i < 0) {
- // _f: resolveFilter
- return ("_f(\"" + filter + "\")(" + exp + ")")
- } else {
- var name = filter.slice(0, i);
- var args = filter.slice(i + 1);
- return ("_f(\"" + name + "\")(" + exp + "," + args)
- }
-}
-
-/* */
-
-var defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;
-var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
-
-var buildRegex = cached(function (delimiters) {
- var open = delimiters[0].replace(regexEscapeRE, '\\$&');
- var close = delimiters[1].replace(regexEscapeRE, '\\$&');
- return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
-});
-
-function parseText (
- text,
- delimiters
-) {
- var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
- if (!tagRE.test(text)) {
- return
- }
- var tokens = [];
- var lastIndex = tagRE.lastIndex = 0;
- var match, index;
- while ((match = tagRE.exec(text))) {
- index = match.index;
- // push text token
- if (index > lastIndex) {
- tokens.push(JSON.stringify(text.slice(lastIndex, index)));
- }
- // tag token
- var exp = parseFilters(match[1].trim());
- tokens.push(("_s(" + exp + ")"));
- lastIndex = index + match[0].length;
- }
- if (lastIndex < text.length) {
- tokens.push(JSON.stringify(text.slice(lastIndex)));
- }
- return tokens.join('+')
-}
-
-/* */
-
-function baseWarn (msg) {
- console.error(("[Vue parser]: " + msg));
-}
-
-function pluckModuleFunction (
- modules,
- key
-) {
- return modules
- ? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })
- : []
-}
-
-function addProp (el, name, value) {
- (el.props || (el.props = [])).push({ name: name, value: value });
-}
-
-function addAttr (el, name, value) {
- (el.attrs || (el.attrs = [])).push({ name: name, value: value });
-}
-
-function addDirective (
- el,
- name,
- rawName,
- value,
- arg,
- modifiers
-) {
- (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
-}
-
-function addHandler (
- el,
- name,
- value,
- modifiers,
- important
-) {
- // check capture modifier
- if (modifiers && modifiers.capture) {
- delete modifiers.capture;
- name = '!' + name; // mark the event as captured
- }
- var events;
- if (modifiers && modifiers.native) {
- delete modifiers.native;
- events = el.nativeEvents || (el.nativeEvents = {});
- } else {
- events = el.events || (el.events = {});
- }
- var newHandler = { value: value, modifiers: modifiers };
- var handlers = events[name];
- /* istanbul ignore if */
- if (Array.isArray(handlers)) {
- important ? handlers.unshift(newHandler) : handlers.push(newHandler);
- } else if (handlers) {
- events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
- } else {
- events[name] = newHandler;
- }
-}
-
-function getBindingAttr (
- el,
- name,
- getStatic
-) {
- var dynamicValue =
- getAndRemoveAttr(el, ':' + name) ||
- getAndRemoveAttr(el, 'v-bind:' + name);
- if (dynamicValue != null) {
- return dynamicValue
- } else if (getStatic !== false) {
- var staticValue = getAndRemoveAttr(el, name);
- if (staticValue != null) {
- return JSON.stringify(staticValue)
- }
- }
-}
-
-function getAndRemoveAttr (el, name) {
- var val;
- if ((val = el.attrsMap[name]) != null) {
- var list = el.attrsList;
- for (var i = 0, l = list.length; i < l; i++) {
- if (list[i].name === name) {
- list.splice(i, 1);
- break
- }
- }
- }
- return val
-}
-
-/* */
-
-var dirRE = /^v-|^@|^:/;
-var forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/;
-var forIteratorRE = /\(([^,]*),([^,]*)(?:,([^,]*))?\)/;
-var bindRE = /^:|^v-bind:/;
-var onRE = /^@|^v-on:/;
-var argRE = /:(.*)$/;
-var modifierRE = /\.[^\.]+/g;
-var specialNewlineRE = /\u2028|\u2029/g;
-
-var decodeHTMLCached = cached(decode);
-
-// configurable state
-var warn$1;
-var platformGetTagNamespace;
-var platformMustUseProp;
-var platformIsPreTag;
-var preTransforms;
-var transforms;
-var postTransforms;
-var delimiters;
-
-/**
- * Convert HTML string to AST.
- */
-function parse (
- template,
- options
-) {
- warn$1 = options.warn || baseWarn;
- platformGetTagNamespace = options.getTagNamespace || no;
- platformMustUseProp = options.mustUseProp || no;
- platformIsPreTag = options.isPreTag || no;
- preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
- transforms = pluckModuleFunction(options.modules, 'transformNode');
- postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
- delimiters = options.delimiters;
- var stack = [];
- var preserveWhitespace = options.preserveWhitespace !== false;
- var root;
- var currentParent;
- var inVPre = false;
- var inPre = false;
- var warned = false;
- parseHTML(template, {
- expectHTML: options.expectHTML,
- isUnaryTag: options.isUnaryTag,
- shouldDecodeNewlines: options.shouldDecodeNewlines,
- start: function start (tag, attrs, unary) {
- // check namespace.
- // inherit parent ns if there is one
- var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
-
- // handle IE svg bug
- /* istanbul ignore if */
- if (options.isIE && ns === 'svg') {
- attrs = guardIESVGBug(attrs);
- }
-
- var element = {
- type: 1,
- tag: tag,
- attrsList: attrs,
- attrsMap: makeAttrsMap(attrs, options.isIE),
- parent: currentParent,
- children: []
- };
- if (ns) {
- element.ns = ns;
- }
-
- if ("client" !== 'server' && isForbiddenTag(element)) {
- element.forbidden = true;
- "development" !== 'production' && warn$1(
- 'Templates should only be responsible for mapping the state to the ' +
- 'UI. Avoid placing tags with side-effects in your templates, such as ' +
- "<" + tag + ">."
- );
- }
-
- // apply pre-transforms
- for (var i = 0; i < preTransforms.length; i++) {
- preTransforms[i](element, options);
- }
-
- if (!inVPre) {
- processPre(element);
- if (element.pre) {
- inVPre = true;
- }
- }
- if (platformIsPreTag(element.tag)) {
- inPre = true;
- }
- if (inVPre) {
- processRawAttrs(element);
- } else {
- processFor(element);
- processIf(element);
- processOnce(element);
- processKey(element);
-
- // determine whether this is a plain element after
- // removing structural attributes
- element.plain = !element.key && !attrs.length;
-
- processRef(element);
- processSlot(element);
- processComponent(element);
- for (var i$1 = 0; i$1 < transforms.length; i$1++) {
- transforms[i$1](element, options);
- }
- processAttrs(element);
- }
-
- function checkRootConstraints (el) {
- {
- if (el.tag === 'slot' || el.tag === 'template') {
- warn$1(
- "Cannot use <" + (el.tag) + "> as component root element because it may " +
- 'contain multiple nodes:\n' + template
- );
- }
- if (el.attrsMap.hasOwnProperty('v-for')) {
- warn$1(
- 'Cannot use v-for on stateful component root element because ' +
- 'it renders multiple elements:\n' + template
- );
- }
- }
- }
-
- // tree management
- if (!root) {
- root = element;
- checkRootConstraints(root);
- } else if ("development" !== 'production' && !stack.length && !warned) {
- // allow 2 root elements with v-if and v-else
- if (root.if && element.else) {
- checkRootConstraints(element);
- root.elseBlock = element;
- } else {
- warned = true;
- warn$1(
- ("Component template should contain exactly one root element:\n\n" + template)
- );
- }
- }
- if (currentParent && !element.forbidden) {
- if (element.else) {
- processElse(element, currentParent);
- } else {
- currentParent.children.push(element);
- element.parent = currentParent;
- }
- }
- if (!unary) {
- currentParent = element;
- stack.push(element);
- }
- // apply post-transforms
- for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
- postTransforms[i$2](element, options);
- }
- },
-
- end: function end () {
- // remove trailing whitespace
- var element = stack[stack.length - 1];
- var lastNode = element.children[element.children.length - 1];
- if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
- element.children.pop();
- }
- // pop stack
- stack.length -= 1;
- currentParent = stack[stack.length - 1];
- // check pre state
- if (element.pre) {
- inVPre = false;
- }
- if (platformIsPreTag(element.tag)) {
- inPre = false;
- }
- },
-
- chars: function chars (text) {
- if (!currentParent) {
- if ("development" !== 'production' && !warned && text === template) {
- warned = true;
- warn$1(
- 'Component template requires a root element, rather than just text:\n\n' + template
- );
- }
- return
- }
- text = inPre || text.trim()
- ? decodeHTMLCached(text)
- // only preserve whitespace if its not right after a starting tag
- : preserveWhitespace && currentParent.children.length ? ' ' : '';
- if (text) {
- var expression;
- if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
- currentParent.children.push({
- type: 2,
- expression: expression,
- text: text
- });
- } else {
- // #3895 special character
- text = text.replace(specialNewlineRE, '');
- currentParent.children.push({
- type: 3,
- text: text
- });
- }
- }
- }
- });
- return root
-}
-
-function processPre (el) {
- if (getAndRemoveAttr(el, 'v-pre') != null) {
- el.pre = true;
- }
-}
-
-function processRawAttrs (el) {
- var l = el.attrsList.length;
- if (l) {
- var attrs = el.attrs = new Array(l);
- for (var i = 0; i < l; i++) {
- attrs[i] = {
- name: el.attrsList[i].name,
- value: JSON.stringify(el.attrsList[i].value)
- };
- }
- } else if (!el.pre) {
- // non root node in pre blocks with no attributes
- el.plain = true;
- }
-}
-
-function processKey (el) {
- var exp = getBindingAttr(el, 'key');
- if (exp) {
- if ("development" !== 'production' && el.tag === 'template') {
- warn$1("<template> cannot be keyed. Place the key on real elements instead.");
- }
- el.key = exp;
- }
-}
-
-function processRef (el) {
- var ref = getBindingAttr(el, 'ref');
- if (ref) {
- el.ref = ref;
- el.refInFor = checkInFor(el);
- }
-}
-
-function processFor (el) {
- var exp;
- if ((exp = getAndRemoveAttr(el, 'v-for'))) {
- var inMatch = exp.match(forAliasRE);
- if (!inMatch) {
- "development" !== 'production' && warn$1(
- ("Invalid v-for expression: " + exp)
- );
- return
- }
- el.for = inMatch[2].trim();
- var alias = inMatch[1].trim();
- var iteratorMatch = alias.match(forIteratorRE);
- if (iteratorMatch) {
- el.alias = iteratorMatch[1].trim();
- el.iterator1 = iteratorMatch[2].trim();
- if (iteratorMatch[3]) {
- el.iterator2 = iteratorMatch[3].trim();
- }
- } else {
- el.alias = alias;
- }
- }
-}
-
-function processIf (el) {
- var exp = getAndRemoveAttr(el, 'v-if');
- if (exp) {
- el.if = exp;
- }
- if (getAndRemoveAttr(el, 'v-else') != null) {
- el.else = true;
- }
-}
-
-function processElse (el, parent) {
- var prev = findPrevElement(parent.children);
- if (prev && prev.if) {
- prev.elseBlock = el;
- } else {
- warn$1(
- ("v-else used on element <" + (el.tag) + "> without corresponding v-if.")
- );
- }
-}
-
-function processOnce (el) {
- var once = getAndRemoveAttr(el, 'v-once');
- if (once != null) {
- el.once = true;
- }
-}
-
-function processSlot (el) {
- if (el.tag === 'slot') {
- el.slotName = getBindingAttr(el, 'name');
- } else {
- var slotTarget = getBindingAttr(el, 'slot');
- if (slotTarget) {
- el.slotTarget = slotTarget;
- }
- }
-}
-
-function processComponent (el) {
- var binding;
- if ((binding = getBindingAttr(el, 'is'))) {
- el.component = binding;
- }
- if (getAndRemoveAttr(el, 'inline-template') != null) {
- el.inlineTemplate = true;
- }
-}
-
-function processAttrs (el) {
- var list = el.attrsList;
- var i, l, name, rawName, value, arg, modifiers, isProp;
- for (i = 0, l = list.length; i < l; i++) {
- name = rawName = list[i].name;
- value = list[i].value;
- if (dirRE.test(name)) {
- // mark element as dynamic
- el.hasBindings = true;
- // modifiers
- modifiers = parseModifiers(name);
- if (modifiers) {
- name = name.replace(modifierRE, '');
- }
- if (bindRE.test(name)) { // v-bind
- name = name.replace(bindRE, '');
- if (modifiers && modifiers.prop) {
- isProp = true;
- name = camelize(name);
- if (name === 'innerHtml') { name = 'innerHTML'; }
- }
- if (isProp || platformMustUseProp(name)) {
- addProp(el, name, value);
- } else {
- addAttr(el, name, value);
- }
- } else if (onRE.test(name)) { // v-on
- name = name.replace(onRE, '');
- addHandler(el, name, value, modifiers);
- } else { // normal directives
- name = name.replace(dirRE, '');
- // parse arg
- var argMatch = name.match(argRE);
- if (argMatch && (arg = argMatch[1])) {
- name = name.slice(0, -(arg.length + 1));
- }
- addDirective(el, name, rawName, value, arg, modifiers);
- if ("development" !== 'production' && name === 'model') {
- checkForAliasModel(el, value);
- }
- }
- } else {
- // literal attribute
- {
- var expression = parseText(value, delimiters);
- if (expression) {
- warn$1(
- name + "=\"" + value + "\": " +
- 'Interpolation inside attributes has been deprecated. ' +
- 'Use v-bind or the colon shorthand instead.'
- );
- }
- }
- addAttr(el, name, JSON.stringify(value));
- }
- }
-}
-
-function checkInFor (el) {
- var parent = el;
- while (parent) {
- if (parent.for !== undefined) {
- return true
- }
- parent = parent.parent;
- }
- return false
-}
-
-function parseModifiers (name) {
- var match = name.match(modifierRE);
- if (match) {
- var ret = {};
- match.forEach(function (m) { ret[m.slice(1)] = true; });
- return ret
- }
-}
-
-function makeAttrsMap (attrs, isIE) {
- var map = {};
- for (var i = 0, l = attrs.length; i < l; i++) {
- if ("development" !== 'production' && map[attrs[i].name] && !isIE) {
- warn$1('duplicate attribute: ' + attrs[i].name);
- }
- map[attrs[i].name] = attrs[i].value;
- }
- return map
-}
-
-function findPrevElement (children) {
- var i = children.length;
- while (i--) {
- if (children[i].tag) { return children[i] }
- }
-}
-
-function isForbiddenTag (el) {
- return (
- el.tag === 'style' ||
- (el.tag === 'script' && (
- !el.attrsMap.type ||
- el.attrsMap.type === 'text/javascript'
- ))
- )
-}
-
-var ieNSBug = /^xmlns:NS\d+/;
-var ieNSPrefix = /^NS\d+:/;
-
-/* istanbul ignore next */
-function guardIESVGBug (attrs) {
- var res = [];
- for (var i = 0; i < attrs.length; i++) {
- var attr = attrs[i];
- if (!ieNSBug.test(attr.name)) {
- attr.name = attr.name.replace(ieNSPrefix, '');
- res.push(attr);
- }
- }
- return res
-}
-
-function checkForAliasModel (el, value) {
- var _el = el;
- while (_el) {
- if (_el.for && _el.alias === value) {
- warn$1(
- "<" + (el.tag) + " v-model=\"" + value + "\">: " +
- "You are binding v-model directly to a v-for iteration alias. " +
- "This will not be able to modify the v-for source array because " +
- "writing to the alias is like modifying a function local variable. " +
- "Consider using an array of objects and use v-model on an object property instead."
- );
- }
- _el = _el.parent;
- }
-}
-
-/* */
-
-var isStaticKey;
-var isPlatformReservedTag;
-
-var genStaticKeysCached = cached(genStaticKeys$1);
-
-/**
- * Goal of the optimizier: walk the generated template AST tree
- * and detect sub-trees that are purely static, i.e. parts of
- * the DOM that never needs to change.
- *
- * Once we detect these sub-trees, we can:
- *
- * 1. Hoist them into constants, so that we no longer need to
- * create fresh nodes for them on each re-render;
- * 2. Completely skip them in the patching process.
- */
-function optimize (root, options) {
- if (!root) { return }
- isStaticKey = genStaticKeysCached(options.staticKeys || '');
- isPlatformReservedTag = options.isReservedTag || (function () { return false; });
- // first pass: mark all non-static nodes.
- markStatic(root);
- // second pass: mark static roots.
- markStaticRoots(root, false);
-}
-
-function genStaticKeys$1 (keys) {
- return makeMap(
- 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
- (keys ? ',' + keys : '')
- )
-}
-
-function markStatic (node) {
- node.static = isStatic(node);
- if (node.type === 1) {
- for (var i = 0, l = node.children.length; i < l; i++) {
- var child = node.children[i];
- markStatic(child);
- if (!child.static) {
- node.static = false;
- }
- }
- }
-}
-
-function markStaticRoots (node, isInFor) {
- if (node.type === 1) {
- if (node.once || node.static) {
- node.staticRoot = true;
- node.staticInFor = isInFor;
- return
- }
- if (node.children) {
- for (var i = 0, l = node.children.length; i < l; i++) {
- markStaticRoots(node.children[i], isInFor || !!node.for);
- }
- }
- }
-}
-
-function isStatic (node) {
- if (node.type === 2) { // expression
- return false
- }
- if (node.type === 3) { // text
- return true
- }
- return !!(node.pre || (
- !node.hasBindings && // no dynamic bindings
- !node.if && !node.for && // not v-if or v-for or v-else
- !isBuiltInTag(node.tag) && // not a built-in
- isPlatformReservedTag(node.tag) && // not a component
- !isDirectChildOfTemplateFor(node) &&
- Object.keys(node).every(isStaticKey)
- ))
-}
-
-function isDirectChildOfTemplateFor (node) {
- while (node.parent) {
- node = node.parent;
- if (node.tag !== 'template') {
- return false
- }
- if (node.for) {
- return true
- }
- }
- return false
-}
-
-/* */
-
-var simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/;
-
-// keyCode aliases
-var keyCodes = {
- esc: 27,
- tab: 9,
- enter: 13,
- space: 32,
- up: 38,
- left: 37,
- right: 39,
- down: 40,
- 'delete': [8, 46]
-};
-
-var modifierCode = {
- stop: '$event.stopPropagation();',
- prevent: '$event.preventDefault();',
- self: 'if($event.target !== $event.currentTarget)return;'
-};
-
-function genHandlers (events, native) {
- var res = native ? 'nativeOn:{' : 'on:{';
- for (var name in events) {
- res += "\"" + name + "\":" + (genHandler(events[name])) + ",";
- }
- return res.slice(0, -1) + '}'
-}
-
-function genHandler (
- handler
-) {
- if (!handler) {
- return 'function(){}'
- } else if (Array.isArray(handler)) {
- return ("[" + (handler.map(genHandler).join(',')) + "]")
- } else if (!handler.modifiers) {
- return simplePathRE.test(handler.value)
- ? handler.value
- : ("function($event){" + (handler.value) + "}")
- } else {
- var code = '';
- var keys = [];
- for (var key in handler.modifiers) {
- if (modifierCode[key]) {
- code += modifierCode[key];
- } else {
- keys.push(key);
- }
- }
- if (keys.length) {
- code = genKeyFilter(keys) + code;
- }
- var handlerCode = simplePathRE.test(handler.value)
- ? handler.value + '($event)'
- : handler.value;
- return 'function($event){' + code + handlerCode + '}'
- }
-}
-
-function genKeyFilter (keys) {
- var code = keys.length === 1
- ? normalizeKeyCode(keys[0])
- : Array.prototype.concat.apply([], keys.map(normalizeKeyCode));
- if (Array.isArray(code)) {
- return ("if(" + (code.map(function (c) { return ("$event.keyCode!==" + c); }).join('&&')) + ")return;")
- } else {
- return ("if($event.keyCode!==" + code + ")return;")
- }
-}
-
-function normalizeKeyCode (key) {
- return (
- parseInt(key, 10) || // number keyCode
- keyCodes[key] || // built-in alias
- ("_k(" + (JSON.stringify(key)) + ")") // custom alias
- )
-}
-
-/* */
-
-function bind$2 (el, dir) {
- el.wrapData = function (code) {
- return ("_b(" + code + "," + (dir.value) + (dir.modifiers && dir.modifiers.prop ? ',true' : '') + ")")
- };
-}
-
-var baseDirectives = {
- bind: bind$2,
- cloak: noop
-};
-
-/* */
-
-// configurable state
-var warn$2;
-var transforms$1;
-var dataGenFns;
-var platformDirectives$1;
-var staticRenderFns;
-var currentOptions;
-
-function generate (
- ast,
- options
-) {
- // save previous staticRenderFns so generate calls can be nested
- var prevStaticRenderFns = staticRenderFns;
- var currentStaticRenderFns = staticRenderFns = [];
- currentOptions = options;
- warn$2 = options.warn || baseWarn;
- transforms$1 = pluckModuleFunction(options.modules, 'transformCode');
- dataGenFns = pluckModuleFunction(options.modules, 'genData');
- platformDirectives$1 = options.directives || {};
- var code = ast ? genElement(ast) : '_h("div")';
- staticRenderFns = prevStaticRenderFns;
- return {
- render: ("with(this){return " + code + "}"),
- staticRenderFns: currentStaticRenderFns
- }
-}
-
-function genElement (el) {
- if (el.staticRoot && !el.staticProcessed) {
- // hoist static sub-trees out
- el.staticProcessed = true;
- staticRenderFns.push(("with(this){return " + (genElement(el)) + "}"));
- return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
- } else if (el.for && !el.forProcessed) {
- return genFor(el)
- } else if (el.if && !el.ifProcessed) {
- return genIf(el)
- } else if (el.tag === 'template' && !el.slotTarget) {
- return genChildren(el) || 'void 0'
- } else if (el.tag === 'slot') {
- return genSlot(el)
- } else {
- // component or element
- var code;
- if (el.component) {
- code = genComponent(el);
- } else {
- var data = genData(el);
- var children = el.inlineTemplate ? null : genChildren(el);
- code = "_h('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
- }
- // module transforms
- for (var i = 0; i < transforms$1.length; i++) {
- code = transforms$1[i](el, code);
- }
- return code
- }
-}
-
-function genIf (el) {
- var exp = el.if;
- el.ifProcessed = true; // avoid recursion
- return ("(" + exp + ")?" + (genElement(el)) + ":" + (genElse(el)))
-}
-
-function genElse (el) {
- return el.elseBlock
- ? genElement(el.elseBlock)
- : '_e()'
-}
-
-function genFor (el) {
- var exp = el.for;
- var alias = el.alias;
- var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
- var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
- el.forProcessed = true; // avoid recursion
- return "_l((" + exp + ")," +
- "function(" + alias + iterator1 + iterator2 + "){" +
- "return " + (genElement(el)) +
- '})'
-}
-
-function genData (el) {
- if (el.plain) {
- return
- }
-
- var data = '{';
-
- // directives first.
- // directives may mutate the el's other properties before they are generated.
- var dirs = genDirectives(el);
- if (dirs) { data += dirs + ','; }
-
- // key
- if (el.key) {
- data += "key:" + (el.key) + ",";
- }
- // ref
- if (el.ref) {
- data += "ref:" + (el.ref) + ",";
- }
- if (el.refInFor) {
- data += "refInFor:true,";
- }
- // record original tag name for components using "is" attribute
- if (el.component) {
- data += "tag:\"" + (el.tag) + "\",";
- }
- // slot target
- if (el.slotTarget) {
- data += "slot:" + (el.slotTarget) + ",";
- }
- // module data generation functions
- for (var i = 0; i < dataGenFns.length; i++) {
- data += dataGenFns[i](el);
- }
- // attributes
- if (el.attrs) {
- data += "attrs:{" + (genProps(el.attrs)) + "},";
- }
- // DOM props
- if (el.props) {
- data += "domProps:{" + (genProps(el.props)) + "},";
- }
- // event handlers
- if (el.events) {
- data += (genHandlers(el.events)) + ",";
- }
- if (el.nativeEvents) {
- data += (genHandlers(el.nativeEvents, true)) + ",";
- }
- // inline-template
- if (el.inlineTemplate) {
- var ast = el.children[0];
- if ("development" !== 'production' && (
- el.children.length > 1 || ast.type !== 1
- )) {
- warn$2('Inline-template components must have exactly one child element.');
- }
- if (ast.type === 1) {
- var inlineRenderFns = generate(ast, currentOptions);
- data += "inlineTemplate:{render:function(){" + (inlineRenderFns.render) + "},staticRenderFns:[" + (inlineRenderFns.staticRenderFns.map(function (code) { return ("function(){" + code + "}"); }).join(',')) + "]}";
- }
- }
- data = data.replace(/,$/, '') + '}';
- // v-bind data wrap
- if (el.wrapData) {
- data = el.wrapData(data);
- }
- return data
-}
-
-function genDirectives (el) {
- var dirs = el.directives;
- if (!dirs) { return }
- var res = 'directives:[';
- var hasRuntime = false;
- var i, l, dir, needRuntime;
- for (i = 0, l = dirs.length; i < l; i++) {
- dir = dirs[i];
- needRuntime = true;
- var gen = platformDirectives$1[dir.name] || baseDirectives[dir.name];
- if (gen) {
- // compile-time directive that manipulates AST.
- // returns true if it also needs a runtime counterpart.
- needRuntime = !!gen(el, dir, warn$2);
- }
- if (needRuntime) {
- hasRuntime = true;
- res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
- }
- }
- if (hasRuntime) {
- return res.slice(0, -1) + ']'
- }
-}
-
-function genChildren (el) {
- if (el.children.length) {
- return '[' + el.children.map(genNode).join(',') + ']'
- }
-}
-
-function genNode (node) {
- if (node.type === 1) {
- return genElement(node)
- } else {
- return genText(node)
- }
-}
-
-function genText (text) {
- return text.type === 2
- ? text.expression // no need for () because already wrapped in _s()
- : JSON.stringify(text.text)
-}
-
-function genSlot (el) {
- var slotName = el.slotName || '"default"';
- var children = genChildren(el);
- return children
- ? ("_t(" + slotName + "," + children + ")")
- : ("_t(" + slotName + ")")
-}
-
-function genComponent (el) {
- var children = el.inlineTemplate ? null : genChildren(el);
- return ("_h(" + (el.component) + "," + (genData(el)) + (children ? ("," + children) : '') + ")")
-}
-
-function genProps (props) {
- var res = '';
- for (var i = 0; i < props.length; i++) {
- var prop = props[i];
- res += "\"" + (prop.name) + "\":" + (prop.value) + ",";
- }
- return res.slice(0, -1)
-}
-
-/* */
-
-/**
- * Compile a template.
- */
-function compile$1 (
- template,
- options
-) {
- var ast = parse(template.trim(), options);
- optimize(ast, options);
- var code = generate(ast, options);
- return {
- ast: ast,
- render: code.render,
- staticRenderFns: code.staticRenderFns
- }
-}
-
-/* */
-
-// operators like typeof, instanceof and in are allowed
-var prohibitedKeywordRE = new RegExp('\\b' + (
- 'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +
- 'super,throw,while,yield,delete,export,import,return,switch,default,' +
- 'extends,finally,continue,debugger,function,arguments'
-).split(',').join('\\b|\\b') + '\\b');
-// check valid identifier for v-for
-var identRE = /[A-Za-z_$][\w$]*/;
-// strip strings in expressions
-var stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g;
-
-// detect problematic expressions in a template
-function detectErrors (ast) {
- var errors = [];
- if (ast) {
- checkNode(ast, errors);
- }
- return errors
-}
-
-function checkNode (node, errors) {
- if (node.type === 1) {
- for (var name in node.attrsMap) {
- if (dirRE.test(name)) {
- var value = node.attrsMap[name];
- if (value) {
- if (name === 'v-for') {
- checkFor(node, ("v-for=\"" + value + "\""), errors);
- } else {
- checkExpression(value, (name + "=\"" + value + "\""), errors);
- }
- }
- }
- }
- if (node.children) {
- for (var i = 0; i < node.children.length; i++) {
- checkNode(node.children[i], errors);
- }
- }
- } else if (node.type === 2) {
- checkExpression(node.expression, node.text, errors);
- }
-}
-
-function checkFor (node, text, errors) {
- checkExpression(node.for || '', text, errors);
- checkIdentifier(node.alias, 'v-for alias', text, errors);
- checkIdentifier(node.iterator1, 'v-for iterator', text, errors);
- checkIdentifier(node.iterator2, 'v-for iterator', text, errors);
-}
-
-function checkIdentifier (ident, type, text, errors) {
- if (typeof ident === 'string' && !identRE.test(ident)) {
- errors.push(("- invalid " + type + " \"" + ident + "\" in expression: " + text));
- }
-}
-
-function checkExpression (exp, text, errors) {
- try {
- new Function(("return " + exp));
- } catch (e) {
- var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);
- if (keywordMatch) {
- errors.push(
- "- avoid using JavaScript keyword as property name: " +
- "\"" + (keywordMatch[0]) + "\" in expression " + text
- );
- } else {
- errors.push(("- invalid expression: " + text));
- }
- }
-}
-
-/* */
-
-function transformNode (el, options) {
- var warn = options.warn || baseWarn;
- var staticClass = getAndRemoveAttr(el, 'class');
- if ("development" !== 'production' && staticClass) {
- var expression = parseText(staticClass, options.delimiters);
- if (expression) {
- warn(
- "class=\"" + staticClass + "\": " +
- 'Interpolation inside attributes has been deprecated. ' +
- 'Use v-bind or the colon shorthand instead.'
- );
- }
- }
- if (staticClass) {
- el.staticClass = JSON.stringify(staticClass);
- }
- var classBinding = getBindingAttr(el, 'class', false /* getStatic */);
- if (classBinding) {
- el.classBinding = classBinding;
- }
-}
-
-function genData$1 (el) {
- var data = '';
- if (el.staticClass) {
- data += "staticClass:" + (el.staticClass) + ",";
- }
- if (el.classBinding) {
- data += "class:" + (el.classBinding) + ",";
- }
- return data
-}
-
-var klass$1 = {
- staticKeys: ['staticClass'],
- transformNode: transformNode,
- genData: genData$1
-};
-
-/* */
-
-function transformNode$1 (el) {
- var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);
- if (styleBinding) {
- el.styleBinding = styleBinding;
- }
-}
-
-function genData$2 (el) {
- return el.styleBinding
- ? ("style:(" + (el.styleBinding) + "),")
- : ''
-}
-
-var style$1 = {
- transformNode: transformNode$1,
- genData: genData$2
-};
-
-var modules$1 = [
- klass$1,
- style$1
-];
-
-/* */
-
-var warn$3;
-
-function model$1 (
- el,
- dir,
- _warn
-) {
- warn$3 = _warn;
- var value = dir.value;
- var modifiers = dir.modifiers;
- var tag = el.tag;
- var type = el.attrsMap.type;
- {
- var dynamicType = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
- if (tag === 'input' && dynamicType) {
- warn$3(
- "<input :type=\"" + dynamicType + "\" v-model=\"" + value + "\">:\n" +
- "v-model does not support dynamic input types. Use v-if branches instead."
- );
- }
- }
- if (tag === 'select') {
- genSelect(el, value);
- } else if (tag === 'input' && type === 'checkbox') {
- genCheckboxModel(el, value);
- } else if (tag === 'input' && type === 'radio') {
- genRadioModel(el, value);
- } else {
- genDefaultModel(el, value, modifiers);
- }
- // ensure runtime directive metadata
- return true
-}
-
-function genCheckboxModel (el, value) {
- if ("development" !== 'production' &&
- el.attrsMap.checked != null) {
- warn$3(
- "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
- "inline checked attributes will be ignored when using v-model. " +
- 'Declare initial values in the component\'s data option instead.'
- );
- }
- var valueBinding = getBindingAttr(el, 'value') || 'null';
- var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';
- var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';
- addProp(el, 'checked',
- "Array.isArray(" + value + ")" +
- "?_i(" + value + "," + valueBinding + ")>-1" +
- ":_q(" + value + "," + trueValueBinding + ")"
- );
- addHandler(el, 'change',
- "var $$a=" + value + "," +
- '$$el=$event.target,' +
- "$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");" +
- 'if(Array.isArray($$a)){' +
- "var $$v=" + valueBinding + "," +
- '$$i=_i($$a,$$v);' +
- "if($$c){$$i<0&&(" + value + "=$$a.concat($$v))}" +
- "else{$$i>-1&&(" + value + "=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}" +
- "}else{" + value + "=$$c}",
- null, true
- );
-}
-
-function genRadioModel (el, value) {
- if ("development" !== 'production' &&
- el.attrsMap.checked != null) {
- warn$3(
- "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
- "inline checked attributes will be ignored when using v-model. " +
- 'Declare initial values in the component\'s data option instead.'
- );
- }
- var valueBinding = getBindingAttr(el, 'value') || 'null';
- addProp(el, 'checked', ("_q(" + value + "," + valueBinding + ")"));
- addHandler(el, 'change', (value + "=" + valueBinding), null, true);
-}
-
-function genDefaultModel (
- el,
- value,
- modifiers
-) {
- {
- if (el.tag === 'input' && el.attrsMap.value) {
- warn$3(
- "<" + (el.tag) + " v-model=\"" + value + "\" value=\"" + (el.attrsMap.value) + "\">:\n" +
- 'inline value attributes will be ignored when using v-model. ' +
- 'Declare initial values in the component\'s data option instead.'
- );
- }
- if (el.tag === 'textarea' && el.children.length) {
- warn$3(
- "<textarea v-model=\"" + value + "\">:\n" +
- 'inline content inside <textarea> will be ignored when using v-model. ' +
- 'Declare initial values in the component\'s data option instead.'
- );
- }
- }
-
- var type = el.attrsMap.type;
- var ref = modifiers || {};
- var lazy = ref.lazy;
- var number = ref.number;
- var trim = ref.trim;
- var event = lazy || (isIE && type === 'range') ? 'change' : 'input';
- var needCompositionGuard = !lazy && type !== 'range';
- var isNative = el.tag === 'input' || el.tag === 'textarea';
-
- var valueExpression = isNative
- ? ("$event.target.value" + (trim ? '.trim()' : ''))
- : "$event";
- var code = number || type === 'number'
- ? (value + "=_n(" + valueExpression + ")")
- : (value + "=" + valueExpression);
- if (isNative && needCompositionGuard) {
- code = "if($event.target.composing)return;" + code;
- }
- // inputs with type="file" are read only and setting the input's
- // value will throw an error.
- if ("development" !== 'production' &&
- type === 'file') {
- warn$3(
- "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
- "File inputs are read only. Use a v-on:change listener instead."
- );
- }
- addProp(el, 'value', isNative ? ("_s(" + value + ")") : ("(" + value + ")"));
- addHandler(el, event, code, null, true);
-}
-
-function genSelect (el, value) {
- {
- el.children.some(checkOptionWarning);
- }
- var code = value + "=Array.prototype.filter" +
- ".call($event.target.options,function(o){return o.selected})" +
- ".map(function(o){return \"_value\" in o ? o._value : o.value})" +
- (el.attrsMap.multiple == null ? '[0]' : '');
- addHandler(el, 'change', code, null, true);
-}
-
-function checkOptionWarning (option) {
- if (option.type === 1 &&
- option.tag === 'option' &&
- option.attrsMap.selected != null) {
- warn$3(
- "<select v-model=\"" + (option.parent.attrsMap['v-model']) + "\">:\n" +
- 'inline selected attributes on <option> will be ignored when using v-model. ' +
- 'Declare initial values in the component\'s data option instead.'
- );
- return true
- }
- return false
-}
-
-/* */
-
-function text (el, dir) {
- if (dir.value) {
- addProp(el, 'textContent', ("_s(" + (dir.value) + ")"));
- }
-}
-
-/* */
-
-function html (el, dir) {
- if (dir.value) {
- addProp(el, 'innerHTML', ("_s(" + (dir.value) + ")"));
- }
-}
-
-var directives$1 = {
- model: model$1,
- text: text,
- html: html
-};
-
-/* */
-
-var cache = Object.create(null);
-
-var baseOptions = {
- isIE: isIE,
- expectHTML: true,
- modules: modules$1,
- staticKeys: genStaticKeys(modules$1),
- directives: directives$1,
- isReservedTag: isReservedTag,
- isUnaryTag: isUnaryTag,
- mustUseProp: mustUseProp,
- getTagNamespace: getTagNamespace,
- isPreTag: isPreTag
-};
-
-function compile$$1 (
- template,
- options
-) {
- options = options
- ? extend(extend({}, baseOptions), options)
- : baseOptions;
- return compile$1(template, options)
-}
-
-function compileToFunctions (
- template,
- options,
- vm
-) {
- var _warn = (options && options.warn) || warn;
- // detect possible CSP restriction
- /* istanbul ignore if */
- {
- try {
- new Function('return 1');
- } catch (e) {
- if (e.toString().match(/unsafe-eval|CSP/)) {
- _warn(
- 'It seems you are using the standalone build of Vue.js in an ' +
- 'environment with Content Security Policy that prohibits unsafe-eval. ' +
- 'The template compiler cannot work in this environment. Consider ' +
- 'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
- 'templates into render functions.'
- );
- }
- }
- }
- var key = options && options.delimiters
- ? String(options.delimiters) + template
- : template;
- if (cache[key]) {
- return cache[key]
- }
- var res = {};
- var compiled = compile$$1(template, options);
- res.render = makeFunction(compiled.render);
- var l = compiled.staticRenderFns.length;
- res.staticRenderFns = new Array(l);
- for (var i = 0; i < l; i++) {
- res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
- }
- {
- if (res.render === noop || res.staticRenderFns.some(function (fn) { return fn === noop; })) {
- _warn(
- "failed to compile template:\n\n" + template + "\n\n" +
- detectErrors(compiled.ast).join('\n') +
- '\n\n',
- vm
- );
- }
- }
- return (cache[key] = res)
-}
-
-function makeFunction (code) {
- try {
- return new Function(code)
- } catch (e) {
- return noop
- }
-}
-
-/* */
-
-var idToTemplate = cached(function (id) {
- var el = query(id);
- return el && el.innerHTML
-});
-
-var mount = Vue$3.prototype.$mount;
-Vue$3.prototype.$mount = function (
- el,
- hydrating
-) {
- el = el && query(el);
-
- /* istanbul ignore if */
- if (el === document.body || el === document.documentElement) {
- "development" !== 'production' && warn(
- "Do not mount Vue to <html> or <body> - mount to normal elements instead."
- );
- return this
- }
-
- var options = this.$options;
- // resolve template/el and convert to render function
- if (!options.render) {
- var template = options.template;
- if (template) {
- if (typeof template === 'string') {
- if (template.charAt(0) === '#') {
- template = idToTemplate(template);
- }
- } else if (template.nodeType) {
- template = template.innerHTML;
- } else {
- {
- warn('invalid template option:' + template, this);
- }
- return this
- }
- } else if (el) {
- template = getOuterHTML(el);
- }
- if (template) {
- var ref = compileToFunctions(template, {
- warn: warn,
- shouldDecodeNewlines: shouldDecodeNewlines,
- delimiters: options.delimiters
- }, this);
- var render = ref.render;
- var staticRenderFns = ref.staticRenderFns;
- options.render = render;
- options.staticRenderFns = staticRenderFns;
- }
- }
- return mount.call(this, el, hydrating)
-};
-
-/**
- * Get outerHTML of elements, taking care
- * of SVG elements in IE as well.
- */
-function getOuterHTML (el) {
- if (el.outerHTML) {
- return el.outerHTML
- } else {
- var container = document.createElement('div');
- container.appendChild(el.cloneNode(true));
- return container.innerHTML
- }
-}
-
-Vue$3.compile = compileToFunctions;
-
-return Vue$3;
-
-})));
diff --git a/vendor/assets/javascripts/vue.js.erb b/vendor/assets/javascripts/vue.js.erb
deleted file mode 100644
index 008beb10f4d..00000000000
--- a/vendor/assets/javascripts/vue.js.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% type = Rails.env.development? ? 'full' : 'min' %>
-<%= File.read(Rails.root.join("vendor/assets/javascripts/vue.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
deleted file mode 100644
index f86786dd454..00000000000
--- a/vendor/assets/javascripts/vue.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * Vue.js v2.0.3
- * (c) 2014-2016 Evan You
- * Released under the MIT License.
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function t(e){var t=parseFloat(e,10);return t||0===t?t:e}function n(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i<r.length;i++)n[r[i]]=!0;return t?function(e){return n[e.toLowerCase()]}:function(e){return n[e]}}function r(e,t){if(e.length){var n=e.indexOf(t);if(n>-1)return e.splice(n,1)}}function i(e,t){return _r.call(e,t)}function a(e){return"string"==typeof e||"number"==typeof e}function o(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}function s(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function c(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e){return null!==e&&"object"==typeof e}function f(e){return kr.call(e)===Ar}function d(e){for(var t={},n=0;n<e.length;n++)e[n]&&u(t,e[n]);return t}function p(){}function v(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}function h(e,t){return e==t||!(!l(e)||!l(t))&&JSON.stringify(e)===JSON.stringify(t)}function m(e,t){for(var n=0;n<e.length;n++)if(h(e[n],t))return n;return-1}function g(e){var t=(e+"").charCodeAt(0);return 36===t||95===t}function y(e,t,n,r){Object.defineProperty(e,t,{value:n,enumerable:!!r,writable:!0,configurable:!0})}function _(e){if(!Sr.test(e)){var t=e.split(".");return function(e){for(var n=0;n<t.length;n++){if(!e)return;e=e[t[n]]}return e}}}function b(e){return/native code/.test(e.toString())}function $(e){Hr.target&&Ur.push(Hr.target),Hr.target=e}function w(){Hr.target=Ur.pop()}function C(){zr.length=0,Vr={},Jr=qr=!1}function x(){for(qr=!0,zr.sort(function(e,t){return e.id-t.id}),Kr=0;Kr<zr.length;Kr++){var e=zr[Kr],t=e.id;Vr[t]=null,e.run()}Ir&&Tr.devtools&&Ir.emit("flush"),C()}function k(e){var t=e.id;if(null==Vr[t]){if(Vr[t]=!0,qr){for(var n=zr.length-1;n>=0&&zr[n].id>e.id;)n--;zr.splice(Math.max(n,Kr)+1,0,e)}else zr.push(e);Jr||(Jr=!0,Br(x))}}function A(e,t){var n,r;t||(t=Gr,t.clear());var i=Array.isArray(e),a=l(e);if((i||a)&&Object.isExtensible(e)){if(e.__ob__){var o=e.__ob__.dep.id;if(t.has(o))return;t.add(o)}if(i)for(n=e.length;n--;)A(e[n],t);else if(a)for(r=Object.keys(e),n=r.length;n--;)A(e[r[n]],t)}}function O(e,t){e.__proto__=t}function T(e,t,n){for(var r=0,i=n.length;r<i;r++){var a=n[r];y(e,a,t[a])}}function S(e){if(l(e)){var t;return i(e,"__ob__")&&e.__ob__ instanceof ti?t=e.__ob__:ei.shouldConvert&&!Tr._isServer&&(Array.isArray(e)||f(e))&&Object.isExtensible(e)&&!e._isVue&&(t=new ti(e)),t}}function E(e,t,n,r){var i=new Hr,a=Object.getOwnPropertyDescriptor(e,t);if(!a||a.configurable!==!1){var o=a&&a.get,s=a&&a.set,c=S(n);Object.defineProperty(e,t,{enumerable:!0,configurable:!0,get:function(){var t=o?o.call(e):n;return Hr.target&&(i.depend(),c&&c.dep.depend(),Array.isArray(t)&&N(t)),t},set:function(t){var r=o?o.call(e):n;t!==r&&(s?s.call(e,t):n=t,c=S(t),i.notify())}})}}function j(e,t,n){if(Array.isArray(e))return e.splice(t,1,n),n;if(i(e,t))return void(e[t]=n);var r=e.__ob__;if(!(e._isVue||r&&r.vmCount))return r?(E(r.value,t,n),r.dep.notify(),n):void(e[t]=n)}function L(e,t){var n=e.__ob__;e._isVue||n&&n.vmCount||i(e,t)&&(delete e[t],n&&n.dep.notify())}function N(e){for(var t=void 0,n=0,r=e.length;n<r;n++)t=e[n],t&&t.__ob__&&t.__ob__.dep.depend(),Array.isArray(t)&&N(t)}function D(e){e._watchers=[],M(e),P(e),R(e),B(e),F(e)}function M(e){var t=e.$options.props;if(t){var n=e.$options.propsData||{},r=e.$options._propKeys=Object.keys(t),i=!e.$parent;ei.shouldConvert=i;for(var a=function(i){var a=r[i];E(e,a,Le(a,t,n,e))},o=0;o<r.length;o++)a(o);ei.shouldConvert=!0}}function P(e){var t=e.$options.data;t=e._data="function"==typeof t?t.call(e):t||{},f(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,a=n.length;a--;)r&&i(r,n[a])||z(e,n[a]);S(t),t.__ob__&&t.__ob__.vmCount++}function R(e){var t=e.$options.computed;if(t)for(var n in t){var r=t[n];"function"==typeof r?(ni.get=I(r,e),ni.set=p):(ni.get=r.get?r.cache!==!1?I(r.get,e):s(r.get,e):p,ni.set=r.set?s(r.set,e):p),Object.defineProperty(e,n,ni)}}function I(e,t){var n=new Zr(t,e,p,{lazy:!0});return function(){return n.dirty&&n.evaluate(),Hr.target&&n.depend(),n.value}}function B(e){var t=e.$options.methods;if(t)for(var n in t)e[n]=null==t[n]?p:s(t[n],e)}function F(e){var t=e.$options.watch;if(t)for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i<r.length;i++)H(e,n,r[i]);else H(e,n,r)}}function H(e,t,n){var r;f(n)&&(r=n,n=n.handler),"string"==typeof n&&(n=e[n]),e.$watch(t,n,r)}function U(e){var t={};t.get=function(){return this._data},Object.defineProperty(e.prototype,"$data",t),e.prototype.$set=j,e.prototype.$delete=L,e.prototype.$watch=function(e,t,n){var r=this;n=n||{},n.user=!0;var i=new Zr(r,e,t,n);return n.immediate&&t.call(r,i.value),function(){i.teardown()}}}function z(e,t){g(t)||Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(n){e._data[t]=n}})}function V(e){var t=new ri(e.tag,e.data,e.children,e.text,e.elm,e.ns,e.context,e.componentOptions);return t.isStatic=e.isStatic,t.key=e.key,t.isCloned=!0,t}function J(e){for(var t=new Array(e.length),n=0;n<e.length;n++)t[n]=V(e[n]);return t}function q(e,t,n,r){r+=t;var i=e.__injected||(e.__injected={});if(!i[r]){i[r]=!0;var a=e[t];a?e[t]=function(){a.apply(this,arguments),n.apply(this,arguments)}:e[t]=n}}function K(e,t,n,r,i){var a,o,s,c,u,l;for(a in e)if(o=e[a],s=t[a],o)if(s){if(o!==s)if(Array.isArray(s)){s.length=o.length;for(var f=0;f<s.length;f++)s[f]=o[f];e[a]=s}else s.fn=o,e[a]=s}else l="!"===a.charAt(0),u=l?a.slice(1):a,Array.isArray(o)?n(u,o.invoker=W(o),l):(o.invoker||(c=o,o=e[a]={},o.fn=c,o.invoker=Z(o)),n(u,o.invoker,l));else;for(a in t)e[a]||(u="!"===a.charAt(0)?a.slice(1):a,r(u,t[a].invoker))}function W(e){return function(t){for(var n=arguments,r=1===arguments.length,i=0;i<e.length;i++)r?e[i](t):e[i].apply(null,n)}}function Z(e){return function(t){var n=1===arguments.length;n?e.fn(t):e.fn.apply(null,arguments)}}function G(e,t,n){if(a(e))return[Y(e)];if(Array.isArray(e)){for(var r=[],i=0,o=e.length;i<o;i++){var s=e[i],c=r[r.length-1];Array.isArray(s)?r.push.apply(r,G(s,t,(n||"")+"_"+i)):a(s)?c&&c.text?c.text+=String(s):""!==s&&r.push(Y(s)):s instanceof ri&&(s.text&&c&&c.text?c.text+=s.text:(t&&Q(s,t),s.tag&&null==s.key&&null!=n&&(s.key="__vlist"+n+"_"+i+"__"),r.push(s)))}return r}}function Y(e){return new ri(void 0,void 0,void 0,String(e))}function Q(e,t){if(e.tag&&!e.ns&&(e.ns=t,e.children))for(var n=0,r=e.children.length;n<r;n++)Q(e.children[n],t)}function X(e){return e&&e.filter(function(e){return e&&e.componentOptions})[0]}function ee(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}function te(e){e.prototype._mount=function(e,t){var n=this;return n.$el=e,n.$options.render||(n.$options.render=ii),ne(n,"beforeMount"),n._watcher=new Zr(n,function(){n._update(n._render(),t)},p),t=!1,null==n.$vnode&&(n._isMounted=!0,ne(n,"mounted")),n},e.prototype._update=function(e,t){var n=this;n._isMounted&&ne(n,"beforeUpdate");var r=n.$el,i=ai;ai=n;var a=n._vnode;n._vnode=e,a?n.$el=n.__patch__(a,e):n.$el=n.__patch__(n.$el,e,t),ai=i,r&&(r.__vue__=null),n.$el&&(n.$el.__vue__=n),n.$vnode&&n.$parent&&n.$vnode===n.$parent._vnode&&(n.$parent.$el=n.$el),n._isMounted&&ne(n,"updated")},e.prototype._updateFromParent=function(e,t,n,r){var i=this,a=!(!i.$options._renderChildren&&!r);if(i.$options._parentVnode=n,i.$options._renderChildren=r,e&&i.$options.props){ei.shouldConvert=!1;for(var o=i.$options._propKeys||[],s=0;s<o.length;s++){var c=o[s];i[c]=Le(c,i.$options.props,e,i)}ei.shouldConvert=!0}if(t){var u=i.$options._parentListeners;i.$options._parentListeners=t,i._updateListeners(t,u)}a&&(i.$slots=_e(r,i._renderContext),i.$forceUpdate())},e.prototype.$forceUpdate=function(){var e=this;e._watcher&&e._watcher.update()},e.prototype.$destroy=function(){var e=this;if(!e._isBeingDestroyed){ne(e,"beforeDestroy"),e._isBeingDestroyed=!0;var t=e.$parent;!t||t._isBeingDestroyed||e.$options.abstract||r(t.$children,e),e._watcher&&e._watcher.teardown();for(var n=e._watchers.length;n--;)e._watchers[n].teardown();e._data.__ob__&&e._data.__ob__.vmCount--,e._isDestroyed=!0,ne(e,"destroyed"),e.$off(),e.$el&&(e.$el.__vue__=null),e.__patch__(e._vnode,null)}}}function ne(e,t){var n=e.$options[t];if(n)for(var r=0,i=n.length;r<i;r++)n[r].call(e);e.$emit("hook:"+t)}function re(e,t,n,r,i){if(e&&(l(e)&&(e=Ce.extend(e)),"function"==typeof e)){if(!e.cid)if(e.resolved)e=e.resolved;else if(e=le(e,function(){n.$forceUpdate()}),!e)return;t=t||{};var a=fe(t,e);if(e.options.functional)return ie(e,a,t,n,r);var o=t.on;t.on=t.nativeOn,e.options.abstract&&(t={}),pe(t);var s=e.options.name||i,c=new ri("vue-component-"+e.cid+(s?"-"+s:""),t,void 0,void 0,void 0,void 0,n,{Ctor:e,propsData:a,listeners:o,tag:i,children:r});return c}}function ie(e,t,n,r,i){var a={},o=e.options.props;if(o)for(var c in o)a[c]=Le(c,o,t);var u=e.options.render.call(null,s(he,{_self:Object.create(r)}),{props:a,data:n,parent:r,children:G(i),slots:function(){return _e(i,r)}});return u instanceof ri&&(u.functionalContext=r,n.slot&&((u.data||(u.data={})).slot=n.slot)),u}function ae(e,t){var n=e.componentOptions,r={_isComponent:!0,parent:t,propsData:n.propsData,_componentTag:n.tag,_parentVnode:e,_parentListeners:n.listeners,_renderChildren:n.children},i=e.data.inlineTemplate;return i&&(r.render=i.render,r.staticRenderFns=i.staticRenderFns),new n.Ctor(r)}function oe(e,t){if(!e.child||e.child._isDestroyed){var n=e.child=ae(e,ai);n.$mount(t?e.elm:void 0,t)}}function se(e,t){var n=t.componentOptions,r=t.child=e.child;r._updateFromParent(n.propsData,n.listeners,t,n.children)}function ce(e){e.child._isMounted||(e.child._isMounted=!0,ne(e.child,"mounted")),e.data.keepAlive&&(e.child._inactive=!1,ne(e.child,"activated"))}function ue(e){e.child._isDestroyed||(e.data.keepAlive?(e.child._inactive=!0,ne(e.child,"deactivated")):e.child.$destroy())}function le(e,t){if(!e.requested){e.requested=!0;var n=e.pendingCallbacks=[t],r=!0,i=function(t){if(l(t)&&(t=Ce.extend(t)),e.resolved=t,!r)for(var i=0,a=n.length;i<a;i++)n[i](t)},a=function(e){},o=e(i,a);return o&&"function"==typeof o.then&&!e.resolved&&o.then(i,a),r=!1,e.resolved}e.pendingCallbacks.push(t)}function fe(e,t){var n=t.options.props;if(n){var r={},i=e.attrs,a=e.props,o=e.domProps;if(i||a||o)for(var s in n){var c=xr(s);de(r,a,s,c,!0)||de(r,i,s,c)||de(r,o,s,c)}return r}}function de(e,t,n,r,a){if(t){if(i(t,n))return e[n]=t[n],a||delete t[n],!0;if(i(t,r))return e[n]=t[r],a||delete t[r],!0}return!1}function pe(e){e.hook||(e.hook={});for(var t=0;t<si.length;t++){var n=si[t],r=e.hook[n],i=oi[n];e.hook[n]=r?ve(i,r):i}}function ve(e,t){return function(n,r){e(n,r),t(n,r)}}function he(e,t,n){return t&&(Array.isArray(t)||"object"!=typeof t)&&(n=t,t=void 0),me(this._self,e,t,n)}function me(e,t,n,r){if(!n||!n.__ob__){if(!t)return ii();if("string"==typeof t){var i,a=Tr.getTagNamespace(t);return Tr.isReservedTag(t)?new ri(t,n,G(r,a),void 0,void 0,a,e):(i=je(e.$options,"components",t))?re(i,n,e,r,t):new ri(t,n,G(r,a),void 0,void 0,a,e)}return re(t,n,e,r)}}function ge(e){e.$vnode=null,e._vnode=null,e._staticTrees=null,e._renderContext=e.$options._parentVnode&&e.$options._parentVnode.context,e.$slots=_e(e.$options._renderChildren,e._renderContext),e.$createElement=s(he,e),e.$options.el&&e.$mount(e.$options.el)}function ye(n){n.prototype.$nextTick=function(e){Br(e,this)},n.prototype._render=function(){var e=this,t=e.$options,n=t.render,r=t.staticRenderFns,i=t._parentVnode;if(e._isMounted)for(var a in e.$slots)e.$slots[a]=J(e.$slots[a]);r&&!e._staticTrees&&(e._staticTrees=[]),e.$vnode=i;var o;try{o=n.call(e._renderProxy,e.$createElement)}catch(t){if(Tr.errorHandler)Tr.errorHandler.call(null,t,e);else{if(Tr._isServer)throw t;setTimeout(function(){throw t},0)}o=e._vnode}return o instanceof ri||(o=ii()),o.parent=i,o},n.prototype._h=he,n.prototype._s=e,n.prototype._n=t,n.prototype._e=ii,n.prototype._q=h,n.prototype._i=m,n.prototype._m=function(e,t){var n=this._staticTrees[e];if(n&&!t)return Array.isArray(n)?J(n):V(n);if(n=this._staticTrees[e]=this.$options.staticRenderFns[e].call(this._renderProxy),Array.isArray(n))for(var r=0;r<n.length;r++)"string"!=typeof n[r]&&(n[r].isStatic=!0,n[r].key="__static__"+e+"_"+r);else n.isStatic=!0,n.key="__static__"+e;return n};var r=function(e){return e};n.prototype._f=function(e){return je(this.$options,"filters",e,!0)||r},n.prototype._l=function(e,t){var n,r,i,a,o;if(Array.isArray(e))for(n=new Array(e.length),r=0,i=e.length;r<i;r++)n[r]=t(e[r],r);else if("number"==typeof e)for(n=new Array(e),r=0;r<e;r++)n[r]=t(r+1,r);else if(l(e))for(a=Object.keys(e),n=new Array(a.length),r=0,i=a.length;r<i;r++)o=a[r],n[r]=t(e[o],o,r);return n},n.prototype._t=function(e,t){var n=this.$slots[e];return n||t},n.prototype._b=function(e,t,n){if(t)if(l(t)){Array.isArray(t)&&(t=d(t));for(var r in t)if("class"===r||"style"===r)e[r]=t[r];else{var i=n||Tr.mustUseProp(r)?e.domProps||(e.domProps={}):e.attrs||(e.attrs={});i[r]=t[r]}}else;return e},n.prototype._k=function(e){return Tr.keyCodes[e]}}function _e(e,t){var n={};if(!e)return n;for(var r,i,a=G(e)||[],o=[],s=0,c=a.length;s<c;s++)if(i=a[s],(i.context===t||i.functionalContext===t)&&i.data&&(r=i.data.slot)){var u=n[r]||(n[r]=[]);"template"===i.tag?u.push.apply(u,i.children):u.push(i)}else o.push(i);return o.length&&(1!==o.length||" "!==o[0].text&&!o[0].isComment)&&(n.default=o),n}function be(e){e._events=Object.create(null);var t=e.$options._parentListeners,n=s(e.$on,e),r=s(e.$off,e);e._updateListeners=function(t,i){K(t,i||{},n,r,e)},t&&e._updateListeners(t)}function $e(e){e.prototype.$on=function(e,t){var n=this;return(n._events[e]||(n._events[e]=[])).push(t),n},e.prototype.$once=function(e,t){function n(){r.$off(e,n),t.apply(r,arguments)}var r=this;return n.fn=t,r.$on(e,n),r},e.prototype.$off=function(e,t){var n=this;if(!arguments.length)return n._events=Object.create(null),n;var r=n._events[e];if(!r)return n;if(1===arguments.length)return n._events[e]=null,n;for(var i,a=r.length;a--;)if(i=r[a],i===t||i.fn===t){r.splice(a,1);break}return n},e.prototype.$emit=function(e){var t=this,n=t._events[e];if(n){n=n.length>1?c(n):n;for(var r=c(arguments,1),i=0,a=n.length;i<a;i++)n[i].apply(t,r)}return t}}function we(e){function t(e,t){var r=e.$options=Object.create(n(e));r.parent=t.parent,r.propsData=t.propsData,r._parentVnode=t._parentVnode,r._parentListeners=t._parentListeners,r._renderChildren=t._renderChildren,r._componentTag=t._componentTag,t.render&&(r.render=t.render,r.staticRenderFns=t.staticRenderFns)}function n(e){var t=e.constructor,n=t.options;if(t.super){var r=t.super.options,i=t.superOptions;r!==i&&(t.superOptions=r,n=t.options=Ee(r,t.extendOptions),n.name&&(n.components[n.name]=t))}return n}e.prototype._init=function(e){var r=this;r._uid=ci++,r._isVue=!0,e&&e._isComponent?t(r,e):r.$options=Ee(n(r),e||{},r),r._renderProxy=r,r._self=r,ee(r),be(r),ne(r,"beforeCreate"),D(r),ne(r,"created"),ge(r)}}function Ce(e){this._init(e)}function xe(e,t){var n,r,a;for(n in t)r=e[n],a=t[n],i(e,n)?l(r)&&l(a)&&xe(r,a):j(e,n,a);return e}function ke(e,t){return t?e?e.concat(t):Array.isArray(t)?t:[t]:e}function Ae(e,t){var n=Object.create(e||null);return t?u(n,t):n}function Oe(e){if(e.components){var t,n=e.components;for(var r in n){var i=r.toLowerCase();yr(i)||Tr.isReservedTag(i)||(t=n[r],f(t)&&(n[r]=Ce.extend(t)))}}}function Te(e){var t=e.props;if(t){var n,r,i,a={};if(Array.isArray(t))for(n=t.length;n--;)r=t[n],"string"==typeof r&&(i=$r(r),a[i]={type:null});else if(f(t))for(var o in t)r=t[o],i=$r(o),a[i]=f(r)?r:{type:r};e.props=a}}function Se(e){var t=e.directives;if(t)for(var n in t){var r=t[n];"function"==typeof r&&(t[n]={bind:r,update:r})}}function Ee(e,t,n){function r(r){var i=fi[r]||di;l[r]=i(e[r],t[r],n,r)}Oe(t),Te(t),Se(t);var a=t.extends;if(a&&(e="function"==typeof a?Ee(e,a.options,n):Ee(e,a,n)),t.mixins)for(var o=0,s=t.mixins.length;o<s;o++){var c=t.mixins[o];c.prototype instanceof Ce&&(c=c.options),e=Ee(e,c,n)}var u,l={};for(u in e)r(u);for(u in t)i(e,u)||r(u);return l}function je(e,t,n,r){if("string"==typeof n){var i=e[t],a=i[n]||i[$r(n)]||i[wr($r(n))];return a}}function Le(e,t,n,r){var a=t[e],o=!i(n,e),s=n[e];if(Me(a.type)&&(o&&!i(a,"default")?s=!1:""!==s&&s!==xr(e)||(s=!0)),void 0===s){s=Ne(r,a,e);var c=ei.shouldConvert;ei.shouldConvert=!0,S(s),ei.shouldConvert=c}return s}function Ne(e,t,n){if(i(t,"default")){var r=t.default;return l(r),"function"==typeof r&&t.type!==Function?r.call(e):r}}function De(e){var t=e&&e.toString().match(/^\s*function (\w+)/);return t&&t[1]}function Me(e){if(!Array.isArray(e))return"Boolean"===De(e);for(var t=0,n=e.length;t<n;t++)if("Boolean"===De(e[t]))return!0;return!1}function Pe(e){e.use=function(e){if(!e.installed){var t=c(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):e.apply(null,t),e.installed=!0,this}}}function Re(e){e.mixin=function(t){e.options=Ee(e.options,t)}}function Ie(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=0===n.cid;if(r&&e._Ctor)return e._Ctor;var i=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=Ee(n.options,e),a.super=n,a.extend=n.extend,Tr._assetTypes.forEach(function(e){a[e]=n[e]}),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=e,r&&(e._Ctor=a),a}}function Be(e){Tr._assetTypes.forEach(function(t){e[t]=function(n,r){return r?("component"===t&&f(r)&&(r.name=r.name||n,r=e.extend(r)),"directive"===t&&"function"==typeof r&&(r={bind:r,update:r}),this.options[t+"s"][n]=r,r):this.options[t+"s"][n]}})}function Fe(e){var t={};t.get=function(){return Tr},Object.defineProperty(e,"config",t),e.util=pi,e.set=j,e.delete=L,e.nextTick=Br,e.options=Object.create(null),Tr._assetTypes.forEach(function(t){e.options[t+"s"]=Object.create(null)}),u(e.options.components,hi),Pe(e),Re(e),Ie(e),Be(e)}function He(e){for(var t=e.data,n=e,r=e;r.child;)r=r.child._vnode,r.data&&(t=Ue(r.data,t));for(;n=n.parent;)n.data&&(t=Ue(t,n.data));return ze(t)}function Ue(e,t){return{staticClass:Ve(e.staticClass,t.staticClass),class:e.class?[e.class,t.class]:t.class}}function ze(e){var t=e.class,n=e.staticClass;return n||t?Ve(n,Je(t)):""}function Ve(e,t){return e?t?e+" "+t:e:t||""}function Je(e){var t="";if(!e)return t;if("string"==typeof e)return e;if(Array.isArray(e)){for(var n,r=0,i=e.length;r<i;r++)e[r]&&(n=Je(e[r]))&&(t+=n+" ");return t.slice(0,-1)}if(l(e)){for(var a in e)e[a]&&(t+=a+" ");return t.slice(0,-1)}return t}function qe(e){return Si(e)?"svg":"math"===e?"math":void 0}function Ke(e){if(!jr)return!0;if(ji(e))return!1;if(e=e.toLowerCase(),null!=Li[e])return Li[e];var t=document.createElement(e);return e.indexOf("-")>-1?Li[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Li[e]=/HTMLUnknownElement/.test(t.toString())}function We(e){if("string"==typeof e){if(e=document.querySelector(e),!e)return document.createElement("div")}return e}function Ze(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&"multiple"in t.data.attrs&&n.setAttribute("multiple","multiple"),n)}function Ge(e,t){return document.createElementNS(xi[e],t)}function Ye(e){return document.createTextNode(e)}function Qe(e){return document.createComment(e)}function Xe(e,t,n){e.insertBefore(t,n)}function et(e,t){e.removeChild(t)}function tt(e,t){e.appendChild(t)}function nt(e){return e.parentNode}function rt(e){return e.nextSibling}function it(e){return e.tagName}function at(e,t){e.textContent=t}function ot(e){return e.childNodes}function st(e,t,n){e.setAttribute(t,n)}function ct(e,t){var n=e.data.ref;if(n){var i=e.context,a=e.child||e.elm,o=i.$refs;t?Array.isArray(o[n])?r(o[n],a):o[n]===a&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])?o[n].push(a):o[n]=[a]:o[n]=a}}function ut(e){return null==e}function lt(e){return null!=e}function ft(e,t){return e.key===t.key&&e.tag===t.tag&&e.isComment===t.isComment&&!e.data==!t.data}function dt(e,t,n){var r,i,a={};for(r=t;r<=n;++r)i=e[r].key,lt(i)&&(a[i]=r);return a}function pt(e){function t(e){return new ri(C.tagName(e).toLowerCase(),{},[],void 0,e)}function n(e,t){function n(){0===--n.listeners&&r(e)}return n.listeners=t,n}function r(e){var t=C.parentNode(e);C.removeChild(t,e)}function i(e,t,n){var r,i=e.data;if(e.isRootInsert=!n,lt(i)&&(lt(r=i.hook)&&lt(r=r.init)&&r(e),lt(r=e.child)))return u(e,t),e.elm;var a=e.children,s=e.tag;return lt(s)?(e.elm=e.ns?C.createElementNS(e.ns,s):C.createElement(s,e),l(e),o(e,a,t),lt(i)&&c(e,t)):e.isComment?e.elm=C.createComment(e.text):e.elm=C.createTextNode(e.text),e.elm}function o(e,t,n){if(Array.isArray(t))for(var r=0;r<t.length;++r)C.appendChild(e.elm,i(t[r],n,!0));else a(e.text)&&C.appendChild(e.elm,C.createTextNode(e.text))}function s(e){for(;e.child;)e=e.child._vnode;return lt(e.tag)}function c(e,t){for(var n=0;n<$.create.length;++n)$.create[n](Mi,e);_=e.data.hook,lt(_)&&(_.create&&_.create(Mi,e),_.insert&&t.push(e))}function u(e,t){e.data.pendingInsert&&t.push.apply(t,e.data.pendingInsert),e.elm=e.child.$el,s(e)?(c(e,t),l(e)):(ct(e),t.push(e))}function l(e){var t;lt(t=e.context)&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,""),lt(t=ai)&&t!==e.context&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,"")}function f(e,t,n,r,a,o){for(;r<=a;++r)C.insertBefore(e,i(n[r],o),t)}function d(e){var t,n,r=e.data;if(lt(r))for(lt(t=r.hook)&&lt(t=t.destroy)&&t(e),t=0;t<$.destroy.length;++t)$.destroy[t](e);if(lt(t=e.children))for(n=0;n<e.children.length;++n)d(e.children[n])}function p(e,t,n,r){for(;n<=r;++n){var i=t[n];lt(i)&&(lt(i.tag)?(v(i),d(i)):C.removeChild(e,i.elm))}}function v(e,t){if(t||lt(e.data)){var i=$.remove.length+1;for(t?t.listeners+=i:t=n(e.elm,i),lt(_=e.child)&&lt(_=_._vnode)&&lt(_.data)&&v(_,t),_=0;_<$.remove.length;++_)$.remove[_](e,t);lt(_=e.data.hook)&&lt(_=_.remove)?_(e,t):t()}else r(e.elm)}function h(e,t,n,r,a){for(var o,s,c,u,l=0,d=0,v=t.length-1,h=t[0],g=t[v],y=n.length-1,_=n[0],b=n[y],$=!a;l<=v&&d<=y;)ut(h)?h=t[++l]:ut(g)?g=t[--v]:ft(h,_)?(m(h,_,r),h=t[++l],_=n[++d]):ft(g,b)?(m(g,b,r),g=t[--v],b=n[--y]):ft(h,b)?(m(h,b,r),$&&C.insertBefore(e,h.elm,C.nextSibling(g.elm)),h=t[++l],b=n[--y]):ft(g,_)?(m(g,_,r),$&&C.insertBefore(e,g.elm,h.elm),g=t[--v],_=n[++d]):(ut(o)&&(o=dt(t,l,v)),s=lt(_.key)?o[_.key]:null,ut(s)?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(c=t[s],c.tag!==_.tag?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(m(c,_,r),t[s]=void 0,$&&C.insertBefore(e,_.elm,h.elm),_=n[++d])));l>v?(u=ut(n[y+1])?null:n[y+1].elm,f(e,u,n,d,y,r)):d>y&&p(e,t,l,v)}function m(e,t,n,r){if(e!==t){if(t.isStatic&&e.isStatic&&t.key===e.key&&t.isCloned)return void(t.elm=e.elm);var i,a=t.data,o=lt(a);o&&lt(i=a.hook)&&lt(i=i.prepatch)&&i(e,t);var c=t.elm=e.elm,u=e.children,l=t.children;if(o&&s(t)){for(i=0;i<$.update.length;++i)$.update[i](e,t);lt(i=a.hook)&&lt(i=i.update)&&i(e,t)}ut(t.text)?lt(u)&&lt(l)?u!==l&&h(c,u,l,n,r):lt(l)?(lt(e.text)&&C.setTextContent(c,""),f(c,null,l,0,l.length-1,n)):lt(u)?p(c,u,0,u.length-1):lt(e.text)&&C.setTextContent(c,""):e.text!==t.text&&C.setTextContent(c,t.text),o&&lt(i=a.hook)&&lt(i=i.postpatch)&&i(e,t)}}function g(e,t,n){if(n&&e.parent)e.parent.data.pendingInsert=t;else for(var r=0;r<t.length;++r)t[r].data.hook.insert(t[r])}function y(e,t,n){t.elm=e;var r=t.tag,i=t.data,a=t.children;if(lt(i)&&(lt(_=i.hook)&&lt(_=_.init)&&_(t,!0),lt(_=t.child)))return u(t,n),!0;if(lt(r)){if(lt(a)){var s=C.childNodes(e);if(s.length){var l=!0;if(s.length!==a.length)l=!1;else for(var f=0;f<a.length;f++)if(!y(s[f],a[f],n)){l=!1;break}if(!l)return!1}else o(t,a,n)}lt(i)&&c(t,n)}return!0}var _,b,$={},w=e.modules,C=e.nodeOps;for(_=0;_<Pi.length;++_)for($[Pi[_]]=[],b=0;b<w.length;++b)void 0!==w[b][Pi[_]]&&$[Pi[_]].push(w[b][Pi[_]]);return function(e,n,r,a){if(!n)return void(e&&d(e));var o,c,u=!1,l=[];if(e){var f=lt(e.nodeType);if(!f&&ft(e,n))m(e,n,l,a);else{if(f){if(1===e.nodeType&&e.hasAttribute("server-rendered")&&(e.removeAttribute("server-rendered"),r=!0),r&&y(e,n,l))return g(n,l,!0),e;e=t(e)}if(o=e.elm,c=C.parentNode(o),i(n,l),n.parent&&(n.parent.elm=n.elm,s(n)))for(var v=0;v<$.create.length;++v)$.create[v](Mi,n.parent);null!==c?(C.insertBefore(c,n.elm,C.nextSibling(o)),p(c,[e],0,0)):lt(e.tag)&&d(e)}}else u=!0,i(n,l);return g(n,l,u),n.elm}}function vt(e,t){if(e.data.directives||t.data.directives){var n,r,i,a=e===Mi,o=ht(e.data.directives,e.context),s=ht(t.data.directives,t.context),c=[],u=[];for(n in s)r=o[n],i=s[n],r?(i.oldValue=r.value,gt(i,"update",t,e),i.def&&i.def.componentUpdated&&u.push(i)):(gt(i,"bind",t,e),i.def&&i.def.inserted&&c.push(i));if(c.length){var l=function(){c.forEach(function(n){gt(n,"inserted",t,e)})};a?q(t.data.hook||(t.data.hook={}),"insert",l,"dir-insert"):l()}if(u.length&&q(t.data.hook||(t.data.hook={}),"postpatch",function(){u.forEach(function(n){gt(n,"componentUpdated",t,e)})},"dir-postpatch"),!a)for(n in o)s[n]||gt(o[n],"unbind",e)}}function ht(e,t){var n=Object.create(null);if(!e)return n;var r,i;for(r=0;r<e.length;r++)i=e[r],i.modifiers||(i.modifiers=Ii),n[mt(i)]=i,i.def=je(t.$options,"directives",i.name,!0);return n}function mt(e){return e.rawName||e.name+"."+Object.keys(e.modifiers||{}).join(".")}function gt(e,t,n,r){var i=e.def&&e.def[t];i&&i(n.elm,e,n,r)}function yt(e,t){if(e.data.attrs||t.data.attrs){var n,r,i,a=t.elm,o=e.data.attrs||{},s=t.data.attrs||{};s.__ob__&&(s=t.data.attrs=u({},s));for(n in s)r=s[n],i=o[n],i!==r&&_t(a,n,r);for(n in o)null==s[n]&&($i(n)?a.removeAttributeNS(bi,wi(n)):yi(n)||a.removeAttribute(n))}}function _t(e,t,n){_i(t)?Ci(n)?e.removeAttribute(t):e.setAttribute(t,t):yi(t)?e.setAttribute(t,Ci(n)||"false"===n?"false":"true"):$i(t)?Ci(n)?e.removeAttributeNS(bi,wi(t)):e.setAttributeNS(bi,t,n):Ci(n)?e.removeAttribute(t):e.setAttribute(t,n)}function bt(e,t){var n=t.elm,r=t.data,i=e.data;if(r.staticClass||r.class||i&&(i.staticClass||i.class)){var a=He(t),o=n._transitionClasses;o&&(a=Ve(a,Je(o))),a!==n._prevClass&&(n.setAttribute("class",a),n._prevClass=a)}}function $t(e,t){if(e.data.on||t.data.on){var n=t.data.on||{},r=e.data.on||{},i=t.elm._v_add||(t.elm._v_add=function(e,n,r){t.elm.addEventListener(e,n,r)}),a=t.elm._v_remove||(t.elm._v_remove=function(e,n){t.elm.removeEventListener(e,n)});K(n,r,i,a,t.context)}}function wt(e,t){if(e.data.domProps||t.data.domProps){var n,r,i=t.elm,a=e.data.domProps||{},o=t.data.domProps||{};o.__ob__&&(o=t.data.domProps=u({},o));for(n in a)null==o[n]&&(i[n]=void 0);for(n in o)if("textContent"!==n&&"innerHTML"!==n||!t.children||(t.children.length=0),r=o[n],"value"===n){i._value=r;var s=null==r?"":String(r);i.value===s||i.composing||(i.value=s)}else i[n]=r}}function Ct(e,t){if(e.data&&e.data.style||t.data.style){var n,r,i=t.elm,a=e.data.style||{},o=t.data.style||{};if("string"==typeof o)return void(i.style.cssText=o);var s=o.__ob__;Array.isArray(o)&&(o=t.data.style=d(o)),s&&(o=t.data.style=u({},o));for(r in a)null==o[r]&&(i.style[Ji(r)]="");for(r in o)n=o[r],n!==a[r]&&(i.style[Ji(r)]=null==n?"":n)}}function xt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+e.getAttribute("class")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function kt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+e.getAttribute("class")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function At(e){ea(function(){ea(e)})}function Ot(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),xt(e,t)}function Tt(e,t){e._transitionClasses&&r(e._transitionClasses,t),kt(e,t)}function St(e,t,n){var r=Et(e,t),i=r.type,a=r.timeout,o=r.propCount;if(!i)return n();var s=i===Wi?Yi:Xi,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=o&&u()};setTimeout(function(){c<o&&u()},a+1),e.addEventListener(s,l)}function Et(e,t){var n,r=window.getComputedStyle(e),i=r[Gi+"Delay"].split(", "),a=r[Gi+"Duration"].split(", "),o=jt(i,a),s=r[Qi+"Delay"].split(", "),c=r[Qi+"Duration"].split(", "),u=jt(s,c),l=0,f=0;t===Wi?o>0&&(n=Wi,l=o,f=a.length):t===Zi?u>0&&(n=Zi,l=u,f=c.length):(l=Math.max(o,u),n=l>0?o>u?Wi:Zi:null,f=n?n===Wi?a.length:c.length:0);var d=n===Wi&&ta.test(r[Gi+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:d}}function jt(e,t){return Math.max.apply(null,t.map(function(t,n){return Lt(t)+Lt(e[n])}))}function Lt(e){return 1e3*Number(e.slice(0,-1))}function Nt(e){var t=e.elm;t._leaveCb&&(t._leaveCb.cancelled=!0,t._leaveCb());var n=Mt(e.data.transition);if(n&&!t._enterCb&&1===t.nodeType){var r=n.css,i=n.type,a=n.enterClass,o=n.enterActiveClass,s=n.appearClass,c=n.appearActiveClass,u=n.beforeEnter,l=n.enter,f=n.afterEnter,d=n.enterCancelled,p=n.beforeAppear,v=n.appear,h=n.afterAppear,m=n.appearCancelled,g=ai.$vnode,y=g&&g.parent?g.parent.context:ai,_=!y._isMounted||!e.isRootInsert;if(!_||v||""===v){var b=_?s:a,$=_?c:o,w=_?p||u:u,C=_&&"function"==typeof v?v:l,x=_?h||f:f,k=_?m||d:d,A=r!==!1&&!Dr,O=C&&(C._length||C.length)>1,T=t._enterCb=Pt(function(){A&&Tt(t,$),T.cancelled?(A&&Tt(t,b),k&&k(t)):x&&x(t),t._enterCb=null});e.data.show||q(e.data.hook||(e.data.hook={}),"insert",function(){var n=t.parentNode,r=n&&n._pending&&n._pending[e.key];r&&r.tag===e.tag&&r.elm._leaveCb&&r.elm._leaveCb(),C&&C(t,T)},"transition-insert"),w&&w(t),A&&(Ot(t,b),Ot(t,$),At(function(){Tt(t,b),T.cancelled||O||St(t,i,T)})),e.data.show&&C&&C(t,T),A||O||T()}}}function Dt(e,t){function n(){m.cancelled||(e.data.show||((r.parentNode._pending||(r.parentNode._pending={}))[e.key]=e),u&&u(r),v&&(Ot(r,s),Ot(r,c),At(function(){Tt(r,s),m.cancelled||h||St(r,o,m)})),l&&l(r,m),v||h||m())}var r=e.elm;r._enterCb&&(r._enterCb.cancelled=!0,r._enterCb());var i=Mt(e.data.transition);if(!i)return t();if(!r._leaveCb&&1===r.nodeType){var a=i.css,o=i.type,s=i.leaveClass,c=i.leaveActiveClass,u=i.beforeLeave,l=i.leave,f=i.afterLeave,d=i.leaveCancelled,p=i.delayLeave,v=a!==!1&&!Dr,h=l&&(l._length||l.length)>1,m=r._leaveCb=Pt(function(){r.parentNode&&r.parentNode._pending&&(r.parentNode._pending[e.key]=null),v&&Tt(r,c),m.cancelled?(v&&Tt(r,s),d&&d(r)):(t(),f&&f(r)),r._leaveCb=null});p?p(n):n()}}function Mt(e){if(e){if("object"==typeof e){var t={};return e.css!==!1&&u(t,na(e.name||"v")),u(t,e),t}return"string"==typeof e?na(e):void 0}}function Pt(e){var t=!1;return function(){t||(t=!0,e())}}function Rt(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var a,o,s=0,c=e.options.length;s<c;s++)if(o=e.options[s],i)a=m(r,Bt(o))>-1,o.selected!==a&&(o.selected=a);else if(h(Bt(o),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function It(e,t){for(var n=0,r=t.length;n<r;n++)if(h(Bt(t[n]),e))return!1;return!0}function Bt(e){return"_value"in e?e._value:e.value}function Ft(e){e.target.composing=!0}function Ht(e){e.target.composing=!1,Ut(e.target,"input")}function Ut(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function zt(e){return!e.child||e.data&&e.data.transition?e:zt(e.child._vnode)}function Vt(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?Vt(X(t.children)):e}function Jt(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var a in i)t[$r(a)]=i[a].fn;return t}function qt(e,t){return/\d-keep-alive$/.test(t.tag)?e("keep-alive"):null}function Kt(e){for(;e=e.parent;)if(e.data.transition)return!0}function Wt(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb();
-}function Zt(e){e.data.newPos=e.elm.getBoundingClientRect()}function Gt(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var a=e.elm.style;a.transform=a.WebkitTransform="translate("+r+"px,"+i+"px)",a.transitionDuration="0s"}}function Yt(e,t){var n=document.createElement("div");return n.innerHTML='<div a="'+e+'">',n.innerHTML.indexOf(t)>0}function Qt(e){return ma.innerHTML=e,ma.textContent}function Xt(e,t){return t&&(e=e.replace(Za,"\n")),e.replace(Ka,"<").replace(Wa,">").replace(Ga,"&").replace(Ya,'"')}function en(e,t){function n(t){f+=t,e=e.substring(t)}function r(){var t=e.match(Ca);if(t){var r={tagName:t[1],attrs:[],start:f};n(t[0].length);for(var i,a;!(i=e.match(xa))&&(a=e.match(ba));)n(a[0].length),r.attrs.push(a);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=f,r}}function i(e){var n=e.tagName,r=e.unarySlash;u&&("p"===s&&Ti(n)&&a("",s),Oi(n)&&s===n&&a("",n));for(var i=l(n)||"html"===n&&"head"===s||!!r,o=e.attrs.length,f=new Array(o),d=0;d<o;d++){var p=e.attrs[d];Oa&&p[0].indexOf('""')===-1&&(""===p[3]&&delete p[3],""===p[4]&&delete p[4],""===p[5]&&delete p[5]);var v=p[3]||p[4]||p[5]||"";f[d]={name:p[1],value:Xt(v,t.shouldDecodeNewlines)}}i||(c.push({tag:n,attrs:f}),s=n,r=""),t.start&&t.start(n,f,i,e.start,e.end)}function a(e,n,r,i){var a;if(null==r&&(r=f),null==i&&(i=f),n){var o=n.toLowerCase();for(a=c.length-1;a>=0&&c[a].tag.toLowerCase()!==o;a--);}else a=0;if(a>=0){for(var u=c.length-1;u>=a;u--)t.end&&t.end(c[u].tag,r,i);c.length=a,s=a&&c[a-1].tag}else"br"===n.toLowerCase()?t.start&&t.start(n,[],!0,r,i):"p"===n.toLowerCase()&&(t.start&&t.start(n,[],!1,r,i),t.end&&t.end(n,r,i))}for(var o,s,c=[],u=t.expectHTML,l=t.isUnaryTag||Or,f=0;e;){if(o=e,s&&Ja(s)){var d=s.toLowerCase(),p=qa[d]||(qa[d]=new RegExp("([\\s\\S]*?)(</"+d+"[^>]*>)","i")),v=0,h=e.replace(p,function(e,n,r){return v=r.length,"script"!==d&&"style"!==d&&"noscript"!==d&&(n=n.replace(/<!--([\s\S]*?)-->/g,"$1").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g,"$1")),t.chars&&t.chars(n),""});f+=e.length-h.length,e=h,a("</"+d+">",d,f-v,f)}else{var m=e.indexOf("<");if(0===m){if(/^<!--/.test(e)){var g=e.indexOf("-->");if(g>=0){n(g+3);continue}}if(/^<!\[/.test(e)){var y=e.indexOf("]>");if(y>=0){n(y+2);continue}}var _=e.match(Aa);if(_){n(_[0].length);continue}var b=e.match(ka);if(b){var $=f;n(b[0].length),a(b[0],b[1],$,f);continue}var w=r();if(w){i(w);continue}}var C=void 0;m>=0?(C=e.substring(0,m),n(m)):(C=e,e=""),t.chars&&t.chars(C)}if(e===o)throw new Error("Error parsing template:\n\n"+e)}a()}function tn(e){function t(){(o||(o=[])).push(e.slice(d,i).trim()),d=i+1}var n,r,i,a,o,s=!1,c=!1,u=0,l=0,f=0,d=0;for(i=0;i<e.length;i++)if(r=n,n=e.charCodeAt(i),s)39===n&&92!==r&&(s=!s);else if(c)34===n&&92!==r&&(c=!c);else if(124!==n||124===e.charCodeAt(i+1)||124===e.charCodeAt(i-1)||u||l||f)switch(n){case 34:c=!0;break;case 39:s=!0;break;case 40:f++;break;case 41:f--;break;case 91:l++;break;case 93:l--;break;case 123:u++;break;case 125:u--}else void 0===a?(d=i+1,a=e.slice(0,i).trim()):t();if(void 0===a?a=e.slice(0,i).trim():0!==d&&t(),o)for(i=0;i<o.length;i++)a=nn(a,o[i]);return a}function nn(e,t){var n=t.indexOf("(");if(n<0)return'_f("'+t+'")('+e+")";var r=t.slice(0,n),i=t.slice(n+1);return'_f("'+r+'")('+e+","+i}function rn(e,t){var n=t?eo(t):Qa;if(n.test(e)){for(var r,i,a=[],o=n.lastIndex=0;r=n.exec(e);){i=r.index,i>o&&a.push(JSON.stringify(e.slice(o,i)));var s=tn(r[1].trim());a.push("_s("+s+")"),o=i+r[0].length}return o<e.length&&a.push(JSON.stringify(e.slice(o))),a.join("+")}}function an(e){console.error("[Vue parser]: "+e)}function on(e,t){return e?e.map(function(e){return e[t]}).filter(function(e){return e}):[]}function sn(e,t,n){(e.props||(e.props=[])).push({name:t,value:n})}function cn(e,t,n){(e.attrs||(e.attrs=[])).push({name:t,value:n})}function un(e,t,n,r,i,a){(e.directives||(e.directives=[])).push({name:t,rawName:n,value:r,arg:i,modifiers:a})}function ln(e,t,n,r,i){r&&r.capture&&(delete r.capture,t="!"+t);var a;r&&r.native?(delete r.native,a=e.nativeEvents||(e.nativeEvents={})):a=e.events||(e.events={});var o={value:n,modifiers:r},s=a[t];Array.isArray(s)?i?s.unshift(o):s.push(o):s?a[t]=i?[o,s]:[s,o]:a[t]=o}function fn(e,t,n){var r=dn(e,":"+t)||dn(e,"v-bind:"+t);if(null!=r)return r;if(n!==!1){var i=dn(e,t);if(null!=i)return JSON.stringify(i)}}function dn(e,t){var n;if(null!=(n=e.attrsMap[t]))for(var r=e.attrsList,i=0,a=r.length;i<a;i++)if(r[i].name===t){r.splice(i,1);break}return n}function pn(e,t){Ta=t.warn||an,Sa=t.getTagNamespace||Or,Ea=t.mustUseProp||Or,ja=t.isPreTag||Or,La=on(t.modules,"preTransformNode"),Na=on(t.modules,"transformNode"),Da=on(t.modules,"postTransformNode"),Ma=t.delimiters;var n,r,i=[],a=t.preserveWhitespace!==!1,o=!1,s=!1;return en(e,{expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,shouldDecodeNewlines:t.shouldDecodeNewlines,start:function(e,a,c){function u(e){}var l=r&&r.ns||Sa(e);t.isIE&&"svg"===l&&(a=En(a));var f={type:1,tag:e,attrsList:a,attrsMap:On(a,t.isIE),parent:r,children:[]};l&&(f.ns=l),Sn(f)&&(f.forbidden=!0);for(var d=0;d<La.length;d++)La[d](f,t);if(o||(vn(f),f.pre&&(o=!0)),ja(f.tag)&&(s=!0),o)hn(f);else{yn(f),_n(f),$n(f),mn(f),f.plain=!f.key&&!a.length,gn(f),wn(f),Cn(f);for(var p=0;p<Na.length;p++)Na[p](f,t);xn(f)}n||(n=f,u(n)),r&&!f.forbidden&&(f.else?bn(f,r):(r.children.push(f),f.parent=r)),c||(r=f,i.push(f));for(var v=0;v<Da.length;v++)Da[v](f,t)},end:function(){var e=i[i.length-1],t=e.children[e.children.length-1];t&&3===t.type&&" "===t.text&&e.children.pop(),i.length-=1,r=i[i.length-1],e.pre&&(o=!1),ja(e.tag)&&(s=!1)},chars:function(e){if(r&&(e=s||e.trim()?uo(e):a&&r.children.length?" ":"")){var t;!o&&" "!==e&&(t=rn(e,Ma))?r.children.push({type:2,expression:t,text:e}):(e=e.replace(co,""),r.children.push({type:3,text:e}))}}}),n}function vn(e){null!=dn(e,"v-pre")&&(e.pre=!0)}function hn(e){var t=e.attrsList.length;if(t)for(var n=e.attrs=new Array(t),r=0;r<t;r++)n[r]={name:e.attrsList[r].name,value:JSON.stringify(e.attrsList[r].value)};else e.pre||(e.plain=!0)}function mn(e){var t=fn(e,"key");t&&(e.key=t)}function gn(e){var t=fn(e,"ref");t&&(e.ref=t,e.refInFor=kn(e))}function yn(e){var t;if(t=dn(e,"v-for")){var n=t.match(no);if(!n)return;e.for=n[2].trim();var r=n[1].trim(),i=r.match(ro);i?(e.alias=i[1].trim(),e.iterator1=i[2].trim(),i[3]&&(e.iterator2=i[3].trim())):e.alias=r}}function _n(e){var t=dn(e,"v-if");t&&(e.if=t),null!=dn(e,"v-else")&&(e.else=!0)}function bn(e,t){var n=Tn(t.children);n&&n.if&&(n.elseBlock=e)}function $n(e){var t=dn(e,"v-once");null!=t&&(e.once=!0)}function wn(e){if("slot"===e.tag)e.slotName=fn(e,"name");else{var t=fn(e,"slot");t&&(e.slotTarget=t)}}function Cn(e){var t;(t=fn(e,"is"))&&(e.component=t),null!=dn(e,"inline-template")&&(e.inlineTemplate=!0)}function xn(e){var t,n,r,i,a,o,s,c,u=e.attrsList;for(t=0,n=u.length;t<n;t++)if(r=i=u[t].name,a=u[t].value,to.test(r))if(e.hasBindings=!0,s=An(r),s&&(r=r.replace(so,"")),io.test(r))r=r.replace(io,""),s&&s.prop&&(c=!0,r=$r(r),"innerHtml"===r&&(r="innerHTML")),c||Ea(r)?sn(e,r,a):cn(e,r,a);else if(ao.test(r))r=r.replace(ao,""),ln(e,r,a,s);else{r=r.replace(to,"");var l=r.match(oo);l&&(o=l[1])&&(r=r.slice(0,-(o.length+1))),un(e,r,i,a,o,s)}else cn(e,r,JSON.stringify(a))}function kn(e){for(var t=e;t;){if(void 0!==t.for)return!0;t=t.parent}return!1}function An(e){var t=e.match(so);if(t){var n={};return t.forEach(function(e){n[e.slice(1)]=!0}),n}}function On(e,t){for(var n={},r=0,i=e.length;r<i;r++)n[e[r].name]=e[r].value;return n}function Tn(e){for(var t=e.length;t--;)if(e[t].tag)return e[t]}function Sn(e){return"style"===e.tag||"script"===e.tag&&(!e.attrsMap.type||"text/javascript"===e.attrsMap.type)}function En(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];lo.test(r.name)||(r.name=r.name.replace(fo,""),t.push(r))}return t}function jn(e,t){e&&(Pa=po(t.staticKeys||""),Ra=t.isReservedTag||function(){return!1},Nn(e),Dn(e,!1))}function Ln(e){return n("type,tag,attrsList,attrsMap,plain,parent,children,attrs"+(e?","+e:""))}function Nn(e){if(e.static=Mn(e),1===e.type)for(var t=0,n=e.children.length;t<n;t++){var r=e.children[t];Nn(r),r.static||(e.static=!1)}}function Dn(e,t){if(1===e.type){if(e.once||e.static)return e.staticRoot=!0,void(e.staticInFor=t);if(e.children)for(var n=0,r=e.children.length;n<r;n++)Dn(e.children[n],t||!!e.for)}}function Mn(e){return 2!==e.type&&(3===e.type||!(!e.pre&&(e.hasBindings||e.if||e.for||yr(e.tag)||!Ra(e.tag)||Pn(e)||!Object.keys(e).every(Pa))))}function Pn(e){for(;e.parent;){if(e=e.parent,"template"!==e.tag)return!1;if(e.for)return!0}return!1}function Rn(e,t){var n=t?"nativeOn:{":"on:{";for(var r in e)n+='"'+r+'":'+In(e[r])+",";return n.slice(0,-1)+"}"}function In(e){if(e){if(Array.isArray(e))return"["+e.map(In).join(",")+"]";if(e.modifiers){var t="",n=[];for(var r in e.modifiers)mo[r]?t+=mo[r]:n.push(r);n.length&&(t=Bn(n)+t);var i=vo.test(e.value)?e.value+"($event)":e.value;return"function($event){"+t+i+"}"}return vo.test(e.value)?e.value:"function($event){"+e.value+"}"}return"function(){}"}function Bn(e){var t=1===e.length?Fn(e[0]):Array.prototype.concat.apply([],e.map(Fn));return Array.isArray(t)?"if("+t.map(function(e){return"$event.keyCode!=="+e}).join("&&")+")return;":"if($event.keyCode!=="+t+")return;"}function Fn(e){return parseInt(e,10)||ho[e]||"_k("+JSON.stringify(e)+")"}function Hn(e,t){e.wrapData=function(e){return"_b("+e+","+t.value+(t.modifiers&&t.modifiers.prop?",true":"")+")"}}function Un(e,t){var n=Ua,r=Ua=[];za=t,Ia=t.warn||an,Ba=on(t.modules,"transformCode"),Fa=on(t.modules,"genData"),Ha=t.directives||{};var i=e?zn(e):'_h("div")';return Ua=n,{render:"with(this){return "+i+"}",staticRenderFns:r}}function zn(e){if(e.staticRoot&&!e.staticProcessed)return e.staticProcessed=!0,Ua.push("with(this){return "+zn(e)+"}"),"_m("+(Ua.length-1)+(e.staticInFor?",true":"")+")";if(e.for&&!e.forProcessed)return qn(e);if(e.if&&!e.ifProcessed)return Vn(e);if("template"!==e.tag||e.slotTarget){if("slot"===e.tag)return Qn(e);var t;if(e.component)t=Xn(e);else{var n=Kn(e),r=e.inlineTemplate?null:Zn(e);t="_h('"+e.tag+"'"+(n?","+n:"")+(r?","+r:"")+")"}for(var i=0;i<Ba.length;i++)t=Ba[i](e,t);return t}return Zn(e)||"void 0"}function Vn(e){var t=e.if;return e.ifProcessed=!0,"("+t+")?"+zn(e)+":"+Jn(e)}function Jn(e){return e.elseBlock?zn(e.elseBlock):"_e()"}function qn(e){var t=e.for,n=e.alias,r=e.iterator1?","+e.iterator1:"",i=e.iterator2?","+e.iterator2:"";return e.forProcessed=!0,"_l(("+t+"),function("+n+r+i+"){return "+zn(e)+"})"}function Kn(e){if(!e.plain){var t="{",n=Wn(e);n&&(t+=n+","),e.key&&(t+="key:"+e.key+","),e.ref&&(t+="ref:"+e.ref+","),e.refInFor&&(t+="refInFor:true,"),e.component&&(t+='tag:"'+e.tag+'",'),e.slotTarget&&(t+="slot:"+e.slotTarget+",");for(var r=0;r<Fa.length;r++)t+=Fa[r](e);if(e.attrs&&(t+="attrs:{"+er(e.attrs)+"},"),e.props&&(t+="domProps:{"+er(e.props)+"},"),e.events&&(t+=Rn(e.events)+","),e.nativeEvents&&(t+=Rn(e.nativeEvents,!0)+","),e.inlineTemplate){var i=e.children[0];if(1===i.type){var a=Un(i,za);t+="inlineTemplate:{render:function(){"+a.render+"},staticRenderFns:["+a.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}return t=t.replace(/,$/,"")+"}",e.wrapData&&(t=e.wrapData(t)),t}}function Wn(e){var t=e.directives;if(t){var n,r,i,a,o="directives:[",s=!1;for(n=0,r=t.length;n<r;n++){i=t[n],a=!0;var c=Ha[i.name]||go[i.name];c&&(a=!!c(e,i,Ia)),a&&(s=!0,o+='{name:"'+i.name+'",rawName:"'+i.rawName+'"'+(i.value?",value:("+i.value+"),expression:"+JSON.stringify(i.value):"")+(i.arg?',arg:"'+i.arg+'"':"")+(i.modifiers?",modifiers:"+JSON.stringify(i.modifiers):"")+"},")}return s?o.slice(0,-1)+"]":void 0}}function Zn(e){if(e.children.length)return"["+e.children.map(Gn).join(",")+"]"}function Gn(e){return 1===e.type?zn(e):Yn(e)}function Yn(e){return 2===e.type?e.expression:JSON.stringify(e.text)}function Qn(e){var t=e.slotName||'"default"',n=Zn(e);return n?"_t("+t+","+n+")":"_t("+t+")"}function Xn(e){var t=e.inlineTemplate?null:Zn(e);return"_h("+e.component+","+Kn(e)+(t?","+t:"")+")"}function er(e){for(var t="",n=0;n<e.length;n++){var r=e[n];t+='"'+r.name+'":'+r.value+","}return t.slice(0,-1)}function tr(e,t){var n=pn(e.trim(),t);jn(n,t);var r=Un(n,t);return{ast:n,render:r.render,staticRenderFns:r.staticRenderFns}}function nr(e,t){var n=(t.warn||an,dn(e,"class"));n&&(e.staticClass=JSON.stringify(n));var r=fn(e,"class",!1);r&&(e.classBinding=r)}function rr(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}function ir(e){var t=fn(e,"style",!1);t&&(e.styleBinding=t)}function ar(e){return e.styleBinding?"style:("+e.styleBinding+"),":""}function or(e,t,n){Va=n;var r=t.value,i=t.modifiers,a=e.tag,o=e.attrsMap.type;return"select"===a?lr(e,r):"input"===a&&"checkbox"===o?sr(e,r):"input"===a&&"radio"===o?cr(e,r):ur(e,r,i),!0}function sr(e,t){var n=fn(e,"value")||"null",r=fn(e,"true-value")||"true",i=fn(e,"false-value")||"false";sn(e,"checked","Array.isArray("+t+")?_i("+t+","+n+")>-1:_q("+t+","+r+")"),ln(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+r+"):("+i+");if(Array.isArray($$a)){var $$v="+n+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+t+"=$$c}",null,!0)}function cr(e,t){var n=fn(e,"value")||"null";sn(e,"checked","_q("+t+","+n+")"),ln(e,"change",t+"="+n,null,!0)}function ur(e,t,n){var r=e.attrsMap.type,i=n||{},a=i.lazy,o=i.number,s=i.trim,c=a||Nr&&"range"===r?"change":"input",u=!a&&"range"!==r,l="input"===e.tag||"textarea"===e.tag,f=l?"$event.target.value"+(s?".trim()":""):"$event",d=o||"number"===r?t+"=_n("+f+")":t+"="+f;l&&u&&(d="if($event.target.composing)return;"+d),sn(e,"value",l?"_s("+t+")":"("+t+")"),ln(e,c,d,null,!0)}function lr(e,t){var n=t+'=Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){return "_value" in o ? o._value : o.value})'+(null==e.attrsMap.multiple?"[0]":"");ln(e,"change",n,null,!0)}function fr(e,t){t.value&&sn(e,"textContent","_s("+t.value+")")}function dr(e,t){t.value&&sn(e,"innerHTML","_s("+t.value+")")}function pr(e,t){return t=t?u(u({},Co),t):Co,tr(e,t)}function vr(e,t,n){var r=(t&&t.warn||li,t&&t.delimiters?String(t.delimiters)+e:e);if(wo[r])return wo[r];var i={},a=pr(e,t);i.render=hr(a.render);var o=a.staticRenderFns.length;i.staticRenderFns=new Array(o);for(var s=0;s<o;s++)i.staticRenderFns[s]=hr(a.staticRenderFns[s]);return wo[r]=i}function hr(e){try{return new Function(e)}catch(e){return p}}function mr(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}var gr,yr=n("slot,component",!0),_r=Object.prototype.hasOwnProperty,br=/-(\w)/g,$r=o(function(e){return e.replace(br,function(e,t){return t?t.toUpperCase():""})}),wr=o(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),Cr=/([^-])([A-Z])/g,xr=o(function(e){return e.replace(Cr,"$1-$2").replace(Cr,"$1-$2").toLowerCase()}),kr=Object.prototype.toString,Ar="[object Object]",Or=function(){return!1},Tr={optionMergeStrategies:Object.create(null),silent:!1,devtools:!1,errorHandler:null,ignoredElements:null,keyCodes:Object.create(null),isReservedTag:Or,isUnknownElement:Or,getTagNamespace:p,mustUseProp:Or,_assetTypes:["component","directive","filter"],_lifecycleHooks:["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed","activated","deactivated"],_maxUpdateCount:100,_isServer:!1},Sr=/[^\w\.\$]/,Er="__proto__"in{},jr="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Lr=jr&&window.navigator.userAgent.toLowerCase(),Nr=Lr&&/msie|trident/.test(Lr),Dr=Lr&&Lr.indexOf("msie 9.0")>0,Mr=Lr&&Lr.indexOf("edge/")>0,Pr=Lr&&Lr.indexOf("android")>0,Rr=Lr&&/iphone|ipad|ipod|ios/.test(Lr),Ir=jr&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Br=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t<e.length;t++)e[t]()}var t,n=[],r=!1;if("undefined"!=typeof Promise&&b(Promise)){var i=Promise.resolve();t=function(){i.then(e),Rr&&setTimeout(p)}}else if("undefined"==typeof MutationObserver||!b(MutationObserver)&&"[object MutationObserverConstructor]"!==MutationObserver.toString())t=function(){setTimeout(e,0)};else{var a=1,o=new MutationObserver(e),s=document.createTextNode(String(a));o.observe(s,{characterData:!0}),t=function(){a=(a+1)%2,s.data=String(a)}}return function(e,i){var a=i?function(){e.call(i)}:e;n.push(a),r||(r=!0,t())}}();gr="undefined"!=typeof Set&&b(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return void 0!==this.set[e]},e.prototype.add=function(e){this.set[e]=1},e.prototype.clear=function(){this.set=Object.create(null)},e}();var Fr=0,Hr=function(){this.id=Fr++,this.subs=[]};Hr.prototype.addSub=function(e){this.subs.push(e)},Hr.prototype.removeSub=function(e){r(this.subs,e)},Hr.prototype.depend=function(){Hr.target&&Hr.target.addDep(this)},Hr.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t<n;t++)e[t].update()},Hr.target=null;var Ur=[],zr=[],Vr={},Jr=!1,qr=!1,Kr=0,Wr=0,Zr=function(e,t,n,r){void 0===r&&(r={}),this.vm=e,e._watchers.push(this),this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.expression=t.toString(),this.cb=n,this.id=++Wr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new gr,this.newDepIds=new gr,"function"==typeof t?this.getter=t:(this.getter=_(t),this.getter||(this.getter=function(){})),this.value=this.lazy?void 0:this.get()};Zr.prototype.get=function(){$(this);var e=this.getter.call(this.vm,this.vm);return this.deep&&A(e),w(),this.cleanupDeps(),e},Zr.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},Zr.prototype.cleanupDeps=function(){for(var e=this,t=this.deps.length;t--;){var n=e.deps[t];e.newDepIds.has(n.id)||n.removeSub(e)}var r=this.depIds;this.depIds=this.newDepIds,this.newDepIds=r,this.newDepIds.clear(),r=this.deps,this.deps=this.newDeps,this.newDeps=r,this.newDeps.length=0},Zr.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():k(this)},Zr.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||l(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){if(!Tr.errorHandler)throw e;Tr.errorHandler.call(null,e,this.vm)}else this.cb.call(this.vm,e,t)}}},Zr.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},Zr.prototype.depend=function(){for(var e=this,t=this.deps.length;t--;)e.deps[t].depend()},Zr.prototype.teardown=function(){var e=this;if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||r(this.vm._watchers,this);for(var t=this.deps.length;t--;)e.deps[t].removeSub(e);this.active=!1}};var Gr=new gr,Yr=Array.prototype,Qr=Object.create(Yr);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(e){var t=Yr[e];y(Qr,e,function(){for(var n=arguments,r=arguments.length,i=new Array(r);r--;)i[r]=n[r];var a,o=t.apply(this,i),s=this.__ob__;switch(e){case"push":a=i;break;case"unshift":a=i;break;case"splice":a=i.slice(2)}return a&&s.observeArray(a),s.dep.notify(),o})});var Xr=Object.getOwnPropertyNames(Qr),ei={shouldConvert:!0,isSettingProps:!1},ti=function(e){if(this.value=e,this.dep=new Hr,this.vmCount=0,y(e,"__ob__",this),Array.isArray(e)){var t=Er?O:T;t(e,Qr,Xr),this.observeArray(e)}else this.walk(e)};ti.prototype.walk=function(e){for(var t=Object.keys(e),n=0;n<t.length;n++)E(e,t[n],e[t[n]])},ti.prototype.observeArray=function(e){for(var t=0,n=e.length;t<n;t++)S(e[t])};var ni={enumerable:!0,configurable:!0,get:p,set:p},ri=function(e,t,n,r,i,a,o,s){this.tag=e,this.data=t,this.children=n,this.text=r,this.elm=i,this.ns=a,this.context=o,this.functionalContext=void 0,this.key=t&&t.key,this.componentOptions=s,this.child=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1},ii=function(){var e=new ri;return e.text="",e.isComment=!0,e},ai=null,oi={init:oe,prepatch:se,insert:ce,destroy:ue},si=Object.keys(oi),ci=0;we(Ce),U(Ce),$e(Ce),te(Ce),ye(Ce);var ui,li=p,fi=Tr.optionMergeStrategies;fi.data=function(e,t,n){return n?e||t?function(){var r="function"==typeof t?t.call(n):t,i="function"==typeof e?e.call(n):void 0;return r?xe(r,i):i}:void 0:t?"function"!=typeof t?e:e?function(){return xe(t.call(this),e.call(this))}:t:e},Tr._lifecycleHooks.forEach(function(e){fi[e]=ke}),Tr._assetTypes.forEach(function(e){fi[e+"s"]=Ae}),fi.watch=function(e,t){if(!t)return e;if(!e)return t;var n={};u(n,e);for(var r in t){var i=n[r],a=t[r];i&&!Array.isArray(i)&&(i=[i]),n[r]=i?i.concat(a):[a]}return n},fi.props=fi.methods=fi.computed=function(e,t){if(!t)return e;if(!e)return t;var n=Object.create(null);return u(n,e),u(n,t),n};var di=function(e,t){return void 0===t?e:t},pi=Object.freeze({defineReactive:E,_toString:e,toNumber:t,makeMap:n,isBuiltInTag:yr,remove:r,hasOwn:i,isPrimitive:a,cached:o,camelize:$r,capitalize:wr,hyphenate:xr,bind:s,toArray:c,extend:u,isObject:l,isPlainObject:f,toObject:d,noop:p,no:Or,genStaticKeys:v,looseEqual:h,looseIndexOf:m,isReserved:g,def:y,parsePath:_,hasProto:Er,inBrowser:jr,UA:Lr,isIE:Nr,isIE9:Dr,isEdge:Mr,isAndroid:Pr,isIOS:Rr,devtools:Ir,nextTick:Br,get _Set(){return gr},mergeOptions:Ee,resolveAsset:je,warn:li,formatComponentName:ui,validateProp:Le}),vi={name:"keep-alive",abstract:!0,created:function(){this.cache=Object.create(null)},render:function(){var e=X(this.$slots.default);if(e&&e.componentOptions){var t=e.componentOptions,n=null==e.key?t.Ctor.cid+"::"+t.tag:e.key;this.cache[n]?e.child=this.cache[n].child:this.cache[n]=e,e.data.keepAlive=!0}return e},destroyed:function(){var e=this;for(var t in this.cache){var n=e.cache[t];ne(n.child,"deactivated"),n.child.$destroy()}}},hi={KeepAlive:vi};Fe(Ce),Object.defineProperty(Ce.prototype,"$isServer",{get:function(){return Tr._isServer}}),Ce.version="2.0.3";var mi,gi=n("value,selected,checked,muted"),yi=n("contenteditable,draggable,spellcheck"),_i=n("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),bi="http://www.w3.org/1999/xlink",$i=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},wi=function(e){return $i(e)?e.slice(6,e.length):""},Ci=function(e){return null==e||e===!1},xi={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},ki=n("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template"),Ai=n("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr",!0),Oi=n("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source",!0),Ti=n("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track",!0),Si=n("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",!0),Ei=function(e){return"pre"===e},ji=function(e){return ki(e)||Si(e)},Li=Object.create(null),Ni=Object.freeze({createElement:Ze,createElementNS:Ge,createTextNode:Ye,createComment:Qe,insertBefore:Xe,removeChild:et,appendChild:tt,parentNode:nt,nextSibling:rt,tagName:it,setTextContent:at,childNodes:ot,setAttribute:st}),Di={create:function(e,t){ct(t)},update:function(e,t){e.data.ref!==t.data.ref&&(ct(e,!0),ct(t))},destroy:function(e){ct(e,!0)}},Mi=new ri("",{},[]),Pi=["create","update","remove","destroy"],Ri={create:vt,update:vt,destroy:function(e){vt(e,Mi)}},Ii=Object.create(null),Bi=[Di,Ri],Fi={create:yt,update:yt},Hi={create:bt,update:bt},Ui={create:$t,update:$t},zi={create:wt,update:wt},Vi=["Webkit","Moz","ms"],Ji=o(function(e){if(mi=mi||document.createElement("div"),e=$r(e),"filter"!==e&&e in mi.style)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=0;n<Vi.length;n++){var r=Vi[n]+t;if(r in mi.style)return r}}),qi={create:Ct,update:Ct},Ki=jr&&!Dr,Wi="transition",Zi="animation",Gi="transition",Yi="transitionend",Qi="animation",Xi="animationend";Ki&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Gi="WebkitTransition",Yi="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Qi="WebkitAnimation",Xi="webkitAnimationEnd"));var ea=jr&&window.requestAnimationFrame||setTimeout,ta=/\b(transform|all)(,|$)/,na=o(function(e){return{enterClass:e+"-enter",leaveClass:e+"-leave",appearClass:e+"-enter",enterActiveClass:e+"-enter-active",leaveActiveClass:e+"-leave-active",appearActiveClass:e+"-enter-active"}}),ra=jr?{create:function(e,t){t.data.show||Nt(t)},remove:function(e,t){e.data.show?t():Dt(e,t)}}:{},ia=[Fi,Hi,Ui,zi,qi,ra],aa=ia.concat(Bi),oa=pt({nodeOps:Ni,modules:aa});Dr&&document.addEventListener("selectionchange",function(){var e=document.activeElement;e&&e.vmodel&&Ut(e,"input")});var sa={inserted:function(e,t,n){if("select"===n.tag){var r=function(){Rt(e,t,n.context)};r(),(Nr||Mr)&&setTimeout(r,0)}else"textarea"!==n.tag&&"text"!==e.type||t.modifiers.lazy||(Pr||(e.addEventListener("compositionstart",Ft),e.addEventListener("compositionend",Ht)),Dr&&(e.vmodel=!0))},componentUpdated:function(e,t,n){if("select"===n.tag){Rt(e,t,n.context);var r=e.multiple?t.value.some(function(t){return It(t,e.options)}):t.value!==t.oldValue&&It(t.value,e.options);r&&Ut(e,"change")}}},ca={bind:function(e,t,n){var r=t.value;n=zt(n);var i=n.data&&n.data.transition;r&&i&&!Dr&&Nt(n);var a="none"===e.style.display?"":e.style.display;e.style.display=r?a:"none",e.__vOriginalDisplay=a},update:function(e,t,n){var r=t.value,i=t.oldValue;if(r!==i){n=zt(n);var a=n.data&&n.data.transition;a&&!Dr?r?(Nt(n),e.style.display=e.__vOriginalDisplay):Dt(n,function(){e.style.display="none"}):e.style.display=r?e.__vOriginalDisplay:"none"}}},ua={model:sa,show:ca},la={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String},fa={name:"transition",props:la,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(function(e){return e.tag}),n.length)){var r=this.mode,i=n[0];if(Kt(this.$vnode))return i;var a=Vt(i);if(!a)return i;if(this._leaving)return qt(e,i);var o=a.key=null==a.key||a.isStatic?"__v"+(a.tag+this._uid)+"__":a.key,s=(a.data||(a.data={})).transition=Jt(this),c=this._vnode,l=Vt(c);if(a.data.directives&&a.data.directives.some(function(e){return"show"===e.name})&&(a.data.show=!0),l&&l.data&&l.key!==o){var f=l.data.transition=u({},s);if("out-in"===r)return this._leaving=!0,q(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()},o),qt(e,i);if("in-out"===r){var d,p=function(){d()};q(s,"afterEnter",p,o),q(s,"enterCancelled",p,o),q(f,"delayLeave",function(e){d=e},o)}}return i}}},da=u({tag:String,moveClass:String},la);delete da.mode;var pa={props:da,render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],a=this.children=[],o=Jt(this),s=0;s<i.length;s++){var c=i[s];c.tag&&null!=c.key&&0!==String(c.key).indexOf("__vlist")&&(a.push(c),n[c.key]=c,(c.data||(c.data={})).transition=o)}if(r){for(var u=[],l=[],f=0;f<r.length;f++){var d=r[f];d.data.transition=o,d.data.pos=d.elm.getBoundingClientRect(),n[d.key]?u.push(d):l.push(d)}this.kept=e(t,null,u),this.removed=l}return e(t,null,a)},beforeUpdate:function(){this.__patch__(this._vnode,this.kept,!1,!0),this._vnode=this.kept},updated:function(){var e=this.prevChildren,t=this.moveClass||this.name+"-move";if(e.length&&this.hasMove(e[0].elm,t)){e.forEach(Wt),e.forEach(Zt),e.forEach(Gt);document.body.offsetHeight;e.forEach(function(e){if(e.data.moved){var n=e.elm,r=n.style;Ot(n,t),r.transform=r.WebkitTransform=r.transitionDuration="",n.addEventListener(Yi,n._moveCb=function e(r){r&&!/transform$/.test(r.propertyName)||(n.removeEventListener(Yi,e),n._moveCb=null,Tt(n,t))})}})}},methods:{hasMove:function(e,t){if(!Ki)return!1;if(null!=this._hasMove)return this._hasMove;Ot(e,t);var n=Et(e);return Tt(e,t),this._hasMove=n.hasTransform}}},va={Transition:fa,TransitionGroup:pa};Ce.config.isUnknownElement=Ke,Ce.config.isReservedTag=ji,Ce.config.getTagNamespace=qe,Ce.config.mustUseProp=gi,u(Ce.options.directives,ua),u(Ce.options.components,va),Ce.prototype.__patch__=Tr._isServer?p:oa,Ce.prototype.$mount=function(e,t){return e=e&&!Tr._isServer?We(e):void 0,this._mount(e,t)},setTimeout(function(){Tr.devtools&&Ir&&Ir.emit("init",Ce)},0);var ha=!!jr&&Yt("\n","&#10;"),ma=document.createElement("div"),ga=/([^\s"'<>\/=]+)/,ya=/(?:=)/,_a=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],ba=new RegExp("^\\s*"+ga.source+"(?:\\s*("+ya.source+")\\s*(?:"+_a.join("|")+"))?"),$a="[a-zA-Z_][\\w\\-\\.]*",wa="((?:"+$a+"\\:)?"+$a+")",Ca=new RegExp("^<"+wa),xa=/^\s*(\/?)>/,ka=new RegExp("^<\\/"+wa+"[^>]*>"),Aa=/^<!DOCTYPE [^>]+>/i,Oa=!1;"x".replace(/x(.)?/g,function(e,t){Oa=""===t});var Ta,Sa,Ea,ja,La,Na,Da,Ma,Pa,Ra,Ia,Ba,Fa,Ha,Ua,za,Va,Ja=n("script,style",!0),qa={},Ka=/&lt;/g,Wa=/&gt;/g,Za=/&#10;/g,Ga=/&amp;/g,Ya=/&quot;/g,Qa=/\{\{((?:.|\n)+?)\}\}/g,Xa=/[-.*+?^${}()|[\]\/\\]/g,eo=o(function(e){var t=e[0].replace(Xa,"\\$&"),n=e[1].replace(Xa,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")}),to=/^v-|^@|^:/,no=/(.*?)\s+(?:in|of)\s+(.*)/,ro=/\(([^,]*),([^,]*)(?:,([^,]*))?\)/,io=/^:|^v-bind:/,ao=/^@|^v-on:/,oo=/:(.*)$/,so=/\.[^\.]+/g,co=/\u2028|\u2029/g,uo=o(Qt),lo=/^xmlns:NS\d+/,fo=/^NS\d+:/,po=o(Ln),vo=/^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/,ho={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},mo={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:"if($event.target !== $event.currentTarget)return;"},go={bind:Hn,cloak:p},yo=(new RegExp("\\b"+"do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,super,throw,while,yield,delete,export,import,return,switch,default,extends,finally,continue,debugger,function,arguments".split(",").join("\\b|\\b")+"\\b"),{staticKeys:["staticClass"],transformNode:nr,genData:rr}),_o={transformNode:ir,genData:ar},bo=[yo,_o],$o={model:or,text:fr,html:dr},wo=Object.create(null),Co={isIE:Nr,expectHTML:!0,modules:bo,staticKeys:v(bo),directives:$o,isReservedTag:ji,isUnaryTag:Ai,mustUseProp:gi,getTagNamespace:qe,isPreTag:Ei},xo=o(function(e){var t=We(e);return t&&t.innerHTML}),ko=Ce.prototype.$mount;return Ce.prototype.$mount=function(e,t){if(e=e&&We(e),e===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=xo(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=mr(e));if(r){var i=vr(r,{warn:li,shouldDecodeNewlines:ha,delimiters:n.delimiters},this),a=i.render,o=i.staticRenderFns;n.render=a,n.staticRenderFns=o}}return ko.call(this,e,t)},Ce.compile=vr,Ce}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js
index 7e24fd9b36e..55438452cad 100644
--- a/vendor/assets/javascripts/xterm/fit.js
+++ b/vendor/assets/javascripts/xterm/fit.js
@@ -16,12 +16,12 @@
/*
* CommonJS environment
*/
- module.exports = fit(require('../../xterm'));
+ module.exports = fit(require('./xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
- define(['../../xterm'], fit);
+ define(['./xterm'], fit);
} else {
/*
* Plain browser environment
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index d028d1251ad..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
@@ -44,3 +45,11 @@ captures/
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index 27ada0591ec..9ea395f15ee 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -1,6 +1,7 @@
CMakeCache.txt
CMakeFiles
CMakeScripts
+Testing
Makefile
cmake_install.cmake
install_manifest.txt
diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore
index 60571a0c383..bfea17cdc5b 100644
--- a/vendor/gitignore/CodeIgniter.gitignore
+++ b/vendor/gitignore/CodeIgniter.gitignore
@@ -9,3 +9,9 @@ user_guide_src/build/*
user_guide_src/cilexer/build/*
user_guide_src/cilexer/dist/*
user_guide_src/cilexer/pycilexer.egg-info/*
+
+#codeigniter 3
+application/logs/*
+!application/logs/index.html
+!application/logs/.htaccess
+/vendor/
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 e375c744b6d..ec7e95c6ab5 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -2,24 +2,25 @@
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
-.idea/workspace.xml
-.idea/tasks.xml
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
# Sensitive or high-churn files:
-.idea/dataSources/
-.idea/dataSources.ids
-.idea/dataSources.xml
-.idea/dataSources.local.xml
-.idea/sqlDataSources.xml
-.idea/dynamic.xml
-.idea/uiDesigner.xml
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
# Gradle:
-.idea/gradle.xml
-.idea/libraries
+.idea/**/gradle.xml
+.idea/**/libraries
# Mongo Explorer plugin:
-.idea/mongoSettings.xml
+.idea/**/mongoSettings.xml
## File-based project format:
*.iws
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index 32a5ad4c777..09dfde64b5f 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -17,3 +17,6 @@ slprj/
# Session info
octave-workspace
+
+# Simulink autosave extension
+.autosave
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/Global/Stata.gitignore b/vendor/gitignore/Global/Stata.gitignore
new file mode 100644
index 00000000000..07997bb1201
--- /dev/null
+++ b/vendor/gitignore/Global/Stata.gitignore
@@ -0,0 +1,24 @@
+# .gitignore file for git projects containing Stata files
+# Commercial statistical software: http://www.stata.com
+
+# Stata dataset and output files
+*.dta
+*.gph
+*.log
+*.smcl
+*.stpr
+*.stsem
+
+# Graphic export files from Stata
+# Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf
+#
+# You may add graphic export files to your .gitignore. However you should be
+# aware that this will exclude all image files from this main directory
+# and subdirectories.
+# *.ps
+# *.eps
+# *.wmf
+# *.emf
+# *.pdf
+# *.png
+# *.tif
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index 5e1047c9d78..a1338d68517 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -1,30 +1,14 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
+# Binaries for programs and plugins
+*.exe
+*.dll
*.so
+*.dylib
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
+# Test binary, build with `go test -c`
*.test
-*.prof
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-# External packages folder
-vendor/
+# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
+.glide/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index e44e0860405..6143e53f9e3 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,5 +1,9 @@
+# Compiled class file
*.class
+# Log file
+*.log
+
# BlueJ files
*.ctxt
@@ -10,6 +14,9 @@
*.jar
*.war
*.ear
+*.zip
+*.tar.gz
+*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 93103fdbe77..53a74e74657 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -29,8 +29,6 @@
/administrator/components/com_search/*
/administrator/components/com_templates/*
/administrator/components/com_users/*
-/administrator/components/com_weblinks/*
-/administrator/components/index.html
/administrator/help/*
/administrator/includes/*
/administrator/language/en-GB/en-GB.com_ajax.ini
@@ -41,7 +39,6 @@
/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini
/administrator/language/en-GB/en-GB.com_postinstall.ini
/administrator/language/en-GB/en-GB.com_postinstall.sys.ini
-/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini
/administrator/language/en-GB/en-GB.com_tags.ini
/administrator/language/en-GB/en-GB.com_tags.sys.ini
/administrator/language/en-GB/en-GB.mod_stats_admin.ini
@@ -250,15 +247,10 @@
/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini
/administrator/language/en-GB/en-GB.plg_user_profile.ini
/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini
/administrator/language/en-GB/en-GB.tpl_hathor.ini
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
-/administrator/language/en-GB/index.html
-/administrator/language/ru-RU/index.html
/administrator/language/overrides/*
-/administrator/language/index.html
/administrator/logs/index.html
/administrator/manifests/*
/administrator/modules/mod_custom/*
@@ -278,12 +270,9 @@
/administrator/modules/mod_unread/*
/administrator/modules/mod_version/*
/administrator/modules/mod_stats_admin/*
-/administrator/modules/index.html
-/administrator/templates/bluestork/*
/administrator/templates/isis/*
/administrator/templates/hathor/*
/administrator/templates/system/*
-/administrator/templates/index.html
/administrator/index.php
/cache/*
/bin/*
@@ -302,7 +291,6 @@
/components/com_newsfeeds/*
/components/com_search/*
/components/com_users/*
-/components/com_weblinks/*
/components/com_wrapper/*
/components/index.html
/images/banners/*
@@ -403,7 +391,6 @@
/language/en-GB/en-GB.tpl_beez5.ini
/language/en-GB/en-GB.tpl_beez5.sys.ini
/language/en-GB/en-GB.xml
-/language/en-GB/index.html
/language/en-GB/install.xml
/language/overrides/*
/language/index.html
@@ -428,8 +415,6 @@
/libraries/index.html
/libraries/import.php
/libraries/loader.php
-/libraries/platform.php
-/logs/*
/media/cms/*
/media/com_contenthistory/*
/media/com_finder/*
@@ -472,7 +457,6 @@
/modules/mod_tags_popular/*
/modules/mod_tags_similar/*
/modules/mod_users_latest/*
-/modules/mod_weblinks/*
/modules/mod_whosonline/*
/modules/mod_wrapper/*
/modules/index.html
@@ -481,9 +465,7 @@
/plugins/authentication/joomla/*
/plugins/authentication/ldap/*
/plugins/authentication/cookie/*
-/plugins/authentication/index.html
/plugins/captcha/recaptcha/*
-/plugins/captcha/index.html
/plugins/content/emailcloak/*
/plugins/content/example/*
/plugins/content/finder/*
@@ -494,27 +476,21 @@
/plugins/content/pagenavigation/*
/plugins/content/vote/*
/plugins/content/contact/*
-/plugins/content/index.html
/plugins/editors/codemirror/*
/plugins/editors/none/*
/plugins/editors/tinymce/*
-/plugins/editors/index.html
/plugins/editors-xtd/module/*
/plugins/editors-xtd/article/*
/plugins/editors-xtd/image/*
/plugins/editors-xtd/pagebreak/*
/plugins/editors-xtd/readmore/*
-/plugins/editors-xtd/index.html
/plugins/extension/example/*
/plugins/extension/joomla/*
-/plugins/extension/index.html
-/plugins/finder/index.html
/plugins/finder/categories/*
/plugins/finder/contacts/*
/plugins/finder/content/*
/plugins/finder/newsfeeds/*
/plugins/finder/tags/*
-/plugins/finder/weblinks/*
/plugins/installer/*
/plugins/quickicon/extensionupdate/*
/plugins/quickicon/joomlaupdate/*
@@ -547,10 +523,7 @@
/plugins/user/profile/*
/plugins/user/index.html
/plugins/index.html
-/templates/atomic/*
/templates/beez3/*
-/templates/beez_20/*
-/templates/beez5/*
/templates/protostar/*
/templates/system/*
/templates/index.html
diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore
index 606ed1c7b4d..208bc4fc591 100644
--- a/vendor/gitignore/KiCad.gitignore
+++ b/vendor/gitignore/KiCad.gitignore
@@ -13,7 +13,8 @@ _autosave-*
*.net
# Autorouter files (exported from Pcbnew)
-.dsn
+*.dsn
+*.ses
# Exported BOM files
*.xml
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index a2d1564060b..a4854bef534 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,5 +1,6 @@
vendor/
node_modules/
+npm-debug.log
# Laravel 4 specific
bootstrap/compiled.php
@@ -7,10 +8,13 @@ app/storage/
# Laravel 5 & Lumen specific
public/storage
+public/hot
storage/*.key
.env.*.php
.env.php
.env
+Homestead.yaml
+Homestead.json
# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
.rocketeer/
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 195c9b7a029..b282f5cf547 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -1,104 +1,16 @@
-.htaccess.sample
-.modgit/
-.modman/
-app/code/community/Phoenix/Moneybookers/
-app/code/community/Cm/RedisSession/
-app/code/core/
-app/design/adminhtml/default/default/
-app/design/frontend/base/
-app/design/frontend/rwd/
-app/design/frontend/default/blank/
-app/design/frontend/default/default/
-app/design/frontend/default/iphone/
-app/design/frontend/default/modern/
-app/design/frontend/enterprise/default
-app/design/install/
-app/etc/modules/Enterprise_*
-app/etc/modules/Mage_*.xml
-app/etc/modules/Phoenix_Moneybookers.xml
-app/etc/modules/Cm_RedisSession.xml
-app/etc/applied.patches.list
-app/etc/config.xml
-app/etc/enterprise.xml
-app/etc/local.xml.additional
-app/etc/local.xml.template
-app/etc/local.xml
-app/.htaccess
-app/bootstrap.php
-app/locale/en_US/
-app/Mage.php
-/cron.php
-cron.sh
-dev/.htaccess
-dev/tests/functional/
-downloader/
-errors/
-favicon.ico
-/get.php
-includes/
-/index.php
-index.php.sample
-/install.php
-js/blank.html
-js/calendar/
-js/enterprise/
-js/extjs/
-js/firebug/
-js/flash/
-js/index.php
-js/jscolor/
-js/lib/
-js/mage/
-js/prototype/
-js/scriptaculous/
-js/spacer.gif
-js/tiny_mce/
-js/varien/
-lib/3Dsecure/
-lib/Apache/
-lib/flex/
-lib/googlecheckout/
-lib/.htaccess
-lib/LinLibertineFont/
-lib/Mage/
-lib/PEAR/
-lib/Pelago/
-lib/phpseclib/
-lib/Varien/
-lib/Zend/
-lib/Cm/
-lib/Credis/
-lib/Magento/
-LICENSE_AFL.txt
-LICENSE.html
-LICENSE.txt
-LICENSE_EE*
-/mage
-media/
-/api.php
-nbproject/
-pear
-pear/
-php.ini.sample
-pkginfo/
-RELEASE_NOTES.txt
-shell/.htaccess
-shell/abstract.php
-shell/compiler.php
-shell/indexer.php
-shell/log.php
-sitemap.xml
-skin/adminhtml/default/default/
-skin/adminhtml/default/enterprise
-skin/frontend/base/
-skin/frontend/rwd/
-skin/frontend/default/blank/
-skin/frontend/default/blue/
-skin/frontend/default/default/
-skin/frontend/default/french/
-skin/frontend/default/german/
-skin/frontend/default/iphone/
-skin/frontend/default/modern/
-skin/frontend/enterprise
-skin/install/
-var/
+#--------------------------#
+# Magento Default Files #
+#--------------------------#
+
+/app/etc/local.xml
+/media/*
+!/media/.htaccess
+!/media/customer/.htaccess
+!/media/dhl/logo.jpg
+!/media/downloadable/.htaccess
+!/media/xmlconnect/custom/ok.gif
+!/media/xmlconnect/original/ok.gif
+!/media/xmlconnect/system/ok.gif
+/var/*
+!/var/.htaccess
+!/var/package/*.xml
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 9a439fcd988..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
@@ -21,6 +23,9 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
+# Bower dependency directory (https://bower.io/)
+bower_components
+
# node-waf configuration
.lock-wscript
@@ -28,8 +33,11 @@ coverage
build/Release
# Dependency directories
-node_modules
-jspm_packages
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
# Optional npm cache directory
.npm
@@ -46,3 +54,6 @@ jspm_packages
# Yarn Integrity file
.yarn-integrity
+# dotenv environment variables file
+.env
+
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 58c51ecaed4..09dfede4814 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
@@ -44,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/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index d41364ab18e..9bf1537f6ae 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -4,6 +4,7 @@
/META.json
/MYMETA.*
*.o
+*.pm.tdy
*.bs
# Devel::Cover
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/PureScript.gitignore b/vendor/gitignore/PureScript.gitignore
new file mode 100644
index 00000000000..361cf5277ba
--- /dev/null
+++ b/vendor/gitignore/PureScript.gitignore
@@ -0,0 +1,8 @@
+# Dependencies
+.psci_modules
+bower_components
+node_modules
+
+# Generated files
+.psci
+output
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 9a05e2debe5..62c1e736924 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -76,11 +76,14 @@ target/
# celery beat schedule file
celerybeat-schedule
+# SageMath parsed files
+*.sage.py
+
# dotenv
.env
# virtualenv
-.venv/
+.venv
venv/
ENV/
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index a02d882cb88..9c07d4ae988 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -1,21 +1,2 @@
*.class
*.log
-
-# sbt specific
-.cache
-.history
-.lib/
-dist/*
-target/
-lib_managed/
-src_managed/
-project/boot/
-project/plugins/project/
-
-# Scala-IDE specific
-.scala_dependencies
-.worksheet
-
-# ENSIME specific
-.ensime_cache/
-.ensime
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 2c22487b5e3..d5340449396 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
@@ -35,6 +36,7 @@ playground.xcworkspace
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
+# Package.pins
.build/
# CocoaPods
@@ -57,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/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 1c10388911b..b829399ae85 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,6 +5,9 @@
/[Bb]uilds/
/Assets/AssetStoreTools*
+# Visual Studio 2015 cache directory
+/.vs/
+
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
@@ -18,6 +21,7 @@ ExportedObj/
*.pidb
*.booproj
*.svd
+*.pdb
# Unity3D generated meta files
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index beec7b91f15..2f096001fec 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -36,6 +36,7 @@
# These project files can be generated by the engine
*.xcodeproj
+*.xcworkspace
*.sln
*.suo
*.opensdf
@@ -56,6 +57,9 @@ Build/*
# Don't ignore icon files in Build
!Build/**/*.ico
+# Built data for maps
+*_BuiltData.uasset
+
# Configuration files generated by the Editor
Saved/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index d9e876cfcdd..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
@@ -199,7 +199,6 @@ ClientBin/
*.jfm
*.pfx
*.publishsettings
-node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
@@ -234,6 +233,10 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
# Visual Studio 6 build log
*.plg
@@ -271,4 +274,14 @@ __pycache__/
*.pyc
# Cake - Uncomment if you are using it
-# tools/
+# 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/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore
index 48e8d8f7be4..dad2b56bdda 100644
--- a/vendor/gitignore/Waf.gitignore
+++ b/vendor/gitignore/Waf.gitignore
@@ -1,4 +1,9 @@
-# for projects that use Waf for building: http://code.google.com/p/waf/
-.waf-*
-.waf3-*
-.lock-*
+# For projects that use the Waf build system: https://waf.io/
+# Dot-hidden on Unix-like systems
+.waf-*-*/
+.waf3-*-*/
+# Hidden directory on Windows (no dot)
+waf-*-*/
+waf3-*-*/
+# Lockfile
+.lock-waf_*_build
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 36dfc539b3b..c644560647f 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# Explaination on the scripts:
+# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/kubernetes-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/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
new file mode 100644
index 00000000000..391b1c7eccf
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,4650 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+abbrev@1, abbrev@1.0.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
+
+accepts@1.3.3, accepts@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
+ dependencies:
+ mime-types "~2.1.11"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.1.tgz#23f671eb6e650dab277fef477c321b1178a8cca2"
+ dependencies:
+ acorn "^4.0.3"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.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"
+
+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"
+
+ajv-keywords@^1.0.0, ajv-keywords@^1.1.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv@^4.7.0:
+ version "4.11.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-escapes@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-html@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+anymatch@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
+ dependencies:
+ arrify "^1.0.0"
+ micromatch "^2.1.5"
+
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ dependencies:
+ default-require-extensions "^1.0.0"
+
+aproba@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.0.tgz#4d8f047a318604e18e3c06a0e52230d3d19f147b"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.0 || ^1.1.13"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b"
+
+array-find@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+
+array-slice@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arraybuffer.slice@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1.js@^4.0.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async@0.2.x, async@~0.2.6:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
+async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.1.2, async@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+ dependencies:
+ lodash "^4.14.0"
+
+async@~0.9.0:
+ version "0.9.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
+ dependencies:
+ chalk "^1.1.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+babel-core@^6.22.1, babel-core@^6.23.0:
+ version "6.23.1"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-generator "^6.23.0"
+ babel-helpers "^6.23.0"
+ babel-messages "^6.23.0"
+ babel-register "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.1"
+ babel-types "^6.23.0"
+ babylon "^6.11.0"
+ convert-source-map "^1.1.0"
+ debug "^2.1.1"
+ json5 "^0.5.0"
+ lodash "^4.2.0"
+ minimatch "^3.0.2"
+ path-is-absolute "^1.0.0"
+ private "^0.1.6"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+
+babel-generator@^6.18.0, babel-generator@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.23.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+babel-helper-bindify-decorators@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.22.0.tgz#d7f5bc261275941ac62acfc4e20dacfb8a3fe952"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.22.0.tgz#29df56be144d81bdeac08262bfa41d2c5e91cdcd"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-call-delegate@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz#119921b56120f17e9dae3f74b4f5cc7bcc1b37ef"
+ dependencies:
+ babel-helper-hoist-variables "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-define-map@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz#1444f960c9691d69a2ced6a205315f8fd00804e7"
+ dependencies:
+ babel-helper-function-name "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.23.0"
+ lodash "^4.2.0"
+
+babel-helper-explode-assignable-expression@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.22.0.tgz#c97bf76eed3e0bae4048121f2b9dae1a4e7d0478"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-explode-class@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.22.0.tgz#646304924aa6388a516843ba7f1855ef8dfeb69b"
+ dependencies:
+ babel-helper-bindify-decorators "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz#25742d67175c8903dbe4b6cb9d9e1fcb8dcf23a6"
+ dependencies:
+ babel-helper-get-function-arity "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+
+babel-helper-get-function-arity@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz#0beb464ad69dc7347410ac6ade9f03a50634f5ce"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-hoist-variables@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz#3eacbf731d80705845dd2e9718f600cfb9b4ba72"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-optimise-call-expression@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz#f3ee7eed355b4282138b33d02b78369e470622f5"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.23.0"
+
+babel-helper-regex@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.22.0.tgz#79f532be1647b1f0ee3474b5f5c3da58001d247d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+ lodash "^4.2.0"
+
+babel-helper-remap-async-to-generator@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.22.0.tgz#2186ae73278ed03b8b15ced089609da981053383"
+ dependencies:
+ babel-helper-function-name "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.22.0"
+ babel-traverse "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-helper-replace-supers@^6.22.0, babel-helper-replace-supers@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz#eeaf8ad9b58ec4337ca94223bacdca1f8d9b4bfd"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.23.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+
+babel-helpers@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.23.0.tgz#4f8f2e092d0b6a8808a4bde79c27f1e2ecf0d992"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+
+babel-loader@^6.2.10:
+ version "6.2.10"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.2.10.tgz#adefc2b242320cd5d15e65b31cea0e8b1b02d4b0"
+ dependencies:
+ find-cache-dir "^0.1.1"
+ loader-utils "^0.2.11"
+ mkdirp "^0.5.1"
+ object-assign "^4.0.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.0.0.tgz#36bde8fbef4837e5ff0366531a2beabd7b1ffa10"
+ dependencies:
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.4.2"
+ test-exclude "^4.0.0"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-async-generators@^6.5.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-decorators@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-generator-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.22.0.tgz#a720a98153a7596f204099cd5409f4b3c05bab46"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.22.0"
+ babel-plugin-syntax-async-generators "^6.5.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-async-to-generator@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.22.0.tgz#194b6938ec195ad36efc4c33a971acf00d8cd35e"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.22.0"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-class-properties@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.23.0.tgz#187b747ee404399013563c993db038f34754ac3b"
+ dependencies:
+ babel-helper-function-name "^6.23.0"
+ babel-plugin-syntax-class-properties "^6.8.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+
+babel-plugin-transform-decorators@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.22.0.tgz#c03635b27a23b23b7224f49232c237a73988d27c"
+ dependencies:
+ babel-helper-explode-class "^6.22.0"
+ babel-plugin-syntax-decorators "^6.13.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.22.0"
+ babel-types "^6.22.0"
+
+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"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz#e48895cf0b375be148cd7c8879b422707a053b51"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+ lodash "^4.2.0"
+
+babel-plugin-transform-es2015-classes@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz#49b53f326202a2fd1b3bbaa5e2edd8a4f78643c1"
+ dependencies:
+ babel-helper-define-map "^6.23.0"
+ babel-helper-function-name "^6.23.0"
+ babel-helper-optimise-call-expression "^6.23.0"
+ babel-helper-replace-supers "^6.23.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.22.0.tgz#7c383e9629bba4820c11b0425bdd6290f7f057e7"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.22.0"
+
+babel-plugin-transform-es2015-destructuring@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.22.0.tgz#672397031c21610d72dd2bbb0ba9fb6277e1c36b"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-plugin-transform-es2015-for-of@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.22.0.tgz#f5fcc8b09093f9a23c76ac3d9e392c3ec4b77104"
+ dependencies:
+ babel-helper-function-name "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ 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"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.22.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"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-types "^6.23.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.23.0.tgz#ae3469227ffac39b0310d90fec73bfdc4f6317b0"
+ dependencies:
+ babel-helper-hoist-variables "^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"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.22.0.tgz#daa60e114a042ea769dd53fe528fc82311eb98fc"
+ dependencies:
+ babel-helper-replace-supers "^6.22.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz#3a2aabb70c8af945d5ce386f1a4250625a83ae3b"
+ dependencies:
+ babel-helper-call-delegate "^6.22.0"
+ babel-helper-get-function-arity "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.23.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz#8ba776e0affaa60bff21e921403b8a652a2ff723"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.22.0.tgz#ab316829e866ee3f4b9eb96939757d19a5bc4593"
+ dependencies:
+ babel-helper-regex "^6.22.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.22.0"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.22.0.tgz#8d9cc27e7ee1decfe65454fb986452a04a613d20"
+ dependencies:
+ babel-helper-regex "^6.22.0"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.22.0.tgz#d57c8335281918e54ef053118ce6eb108468084d"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.22.0"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-object-rest-spread@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.22.0.tgz#65740593a319c44522157538d690b84094617ea6"
+ dependencies:
+ regenerator-transform "0.9.8"
+
+babel-plugin-transform-strict-mode@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz#e008df01340fdc87e959da65991b7e05970c8c7c"
+ dependencies:
+ 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"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.22.0"
+ babel-plugin-transform-es2015-classes "^6.22.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.22.0"
+ babel-plugin-transform-es2015-duplicate-keys "^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-systemjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-umd "^6.22.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"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-regenerator "^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"
+ dependencies:
+ babel-plugin-syntax-dynamic-import "^6.18.0"
+ babel-plugin-transform-class-properties "^6.22.0"
+ babel-plugin-transform-decorators "^6.22.0"
+ babel-preset-stage-3 "^6.22.0"
+
+babel-preset-stage-3@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.22.0.tgz#a4e92bbace7456fafdf651d7a7657ee0bbca9c2e"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-generator-functions "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-object-rest-spread "^6.22.0"
+
+babel-register@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.23.0.tgz#c9aa3d4cca94b51da34826c4a0f9e08145d74ff3"
+ dependencies:
+ babel-core "^6.23.0"
+ babel-runtime "^6.22.0"
+ core-js "^2.4.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.2.0"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.2"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.10.0"
+
+babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.23.0"
+ babel-types "^6.23.0"
+ babylon "^6.11.0"
+ lodash "^4.2.0"
+
+babel-traverse@^6.18.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.23.1:
+ version "6.23.1"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.23.0"
+ babylon "^6.15.0"
+ debug "^2.2.0"
+ globals "^9.0.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf"
+ dependencies:
+ babel-runtime "^6.22.0"
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^1.0.1"
+
+babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0:
+ version "6.15.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
+
+backo2@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+
+balanced-match@^0.4.1:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+
+base64-arraybuffer@0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
+
+base64-js@^1.0.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
+
+base64id@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
+
+batch@0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+better-assert@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
+ dependencies:
+ callsite "1.0.0"
+
+big.js@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
+
+binary-extensions@^1.0.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774"
+
+blob@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+bluebird@^3.3.0:
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.6"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
+
+body-parser@^1.12.4:
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b"
+ dependencies:
+ bytes "2.4.0"
+ content-type "~1.0.2"
+ debug "2.6.0"
+ depd "~1.1.0"
+ http-errors "~1.5.1"
+ iconv-lite "0.4.15"
+ on-finished "~2.3.0"
+ qs "6.2.1"
+ raw-body "~2.2.0"
+ type-is "~1.6.14"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+bootstrap-sass@^3.3.6:
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
+
+brace-expansion@^1.0.0:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9"
+ dependencies:
+ balanced-match "^0.4.1"
+ concat-map "0.0.1"
+
+braces@^0.1.2:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6"
+ dependencies:
+ expand-range "^0.1.0"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+brorand@^1.0.1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.7.tgz#6677fa5e4901bdbf9c9ec2a748e28dca407a9bfc"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a"
+ dependencies:
+ buffer-xor "^1.0.2"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.0.tgz#10773910c3c206d5420a46aad8694f820b85968f"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+ dependencies:
+ pako "~0.2.0"
+
+buffer-shims@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
+
+buffer-xor@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+bytes@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
+
+bytes@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsite@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+caseless@~0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07"
+ dependencies:
+ inherits "^2.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
+
+cli-cursor@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ dependencies:
+ restore-cursor "^1.0.1"
+
+cli-width@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+clone@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+colors@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+combine-lists@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6"
+ dependencies:
+ lodash "^4.5.0"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.8.1, commander@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
+component-bind@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
+
+component-emitter@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3"
+
+component-emitter@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+component-inherit@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
+
+compressible@~2.0.8:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.9.tgz#6daab4e2b599c2770dd9e21e7a891b1c5a755425"
+ dependencies:
+ mime-db ">= 1.24.0 < 2"
+
+compression-webpack-plugin@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-0.3.2.tgz#1edfb0e749d7366d3e701670c463359b2c0cf704"
+ dependencies:
+ async "0.2.x"
+ webpack-sources "^0.1.0"
+ optionalDependencies:
+ node-zopfli "^2.0.0"
+
+compression@^1.5.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3"
+ dependencies:
+ accepts "~1.3.3"
+ bytes "2.3.0"
+ compressible "~2.0.8"
+ debug "~2.2.0"
+ on-headers "~1.0.1"
+ vary "~1.1.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "~2.0.0"
+ typedarray "~0.0.5"
+
+concat-stream@^1.4.6:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+connect-history-api-fallback@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
+
+connect@^3.3.5:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198"
+ dependencies:
+ debug "~2.2.0"
+ finalhandler "0.5.0"
+ parseurl "~1.3.1"
+ utils-merge "1.0.0"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+
+content-type@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
+
+convert-source-map@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+
+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.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.2.tgz#51210062d7bb7479f6c65bb41a92208b1d61abad"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^1.0.0"
+ sha.js "^2.3.6"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.4.tgz#d3fb4ba253eb8b3f56e39ea2fbcb8af747bd3170"
+ dependencies:
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+
+custom-event@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+
+d3@^3.5.11:
+ version "3.5.11"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
+
+d@^0.1.1, d@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
+ dependencies:
+ es5-ext "~0.10.2"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+debug@0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
+
+debug@2.2.0, debug@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+ dependencies:
+ ms "0.7.1"
+
+debug@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
+ dependencies:
+ ms "0.7.2"
+
+debug@2.6.0, debug@^2.1.1, debug@^2.2.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
+ dependencies:
+ ms "0.7.2"
+
+decamelize@^1.0.0, decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+deep-extend@~0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ dependencies:
+ strip-bom "^2.0.0"
+
+defaults@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+ dependencies:
+ clone "^1.0.2"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+di@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+doctrine@1.5.0, doctrine@^1.2.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ 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"
+ dependencies:
+ custom-event "~1.0.0"
+ ent "~2.2.0"
+ extend "^3.0.0"
+ void-elements "^2.0.0"
+
+domain-browser@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+
+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"
+ dependencies:
+ jsbn "~0.1.0"
+
+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"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ 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"
+
+encodeurl@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
+
+engine.io-client@1.8.2:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766"
+ dependencies:
+ component-emitter "1.2.1"
+ component-inherit "0.0.3"
+ debug "2.3.3"
+ engine.io-parser "1.3.2"
+ has-cors "1.1.0"
+ indexof "0.0.1"
+ parsejson "0.0.3"
+ parseqs "0.0.5"
+ parseuri "0.0.5"
+ ws "1.1.1"
+ xmlhttprequest-ssl "1.5.3"
+ yeast "0.1.2"
+
+engine.io-parser@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a"
+ dependencies:
+ after "0.8.2"
+ arraybuffer.slice "0.0.6"
+ base64-arraybuffer "0.1.5"
+ blob "0.0.4"
+ has-binary "0.1.7"
+ wtf-8 "1.0.0"
+
+engine.io@1.8.2:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.2.tgz#6b59be730b348c0125b0a4589de1c355abcf7a7e"
+ dependencies:
+ accepts "1.3.3"
+ base64id "1.0.0"
+ cookie "0.3.1"
+ debug "2.3.3"
+ engine.io-parser "1.3.2"
+ ws "1.1.1"
+
+enhanced-resolve@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ object-assign "^4.0.1"
+ tapable "^0.2.5"
+
+enhanced-resolve@~0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.2.0"
+ tapable "^0.1.8"
+
+ent@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+
+errno@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
+ dependencies:
+ prr "~0.0.0"
+
+error-ex@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7:
+ version "0.10.12"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
+ dependencies:
+ es6-iterator "2"
+ es6-symbol "~3.1"
+
+es6-iterator@2:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac"
+ dependencies:
+ d "^0.1.1"
+ es5-ext "^0.10.7"
+ es6-symbol "3"
+
+es6-map@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897"
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.11"
+ es6-iterator "2"
+ es6-set "~0.1.3"
+ es6-symbol "~3.1.0"
+ event-emitter "~0.3.4"
+
+es6-promise@~4.0.3:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
+
+es6-set@~0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8"
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.11"
+ es6-iterator "2"
+ es6-symbol "3"
+ event-emitter "~0.3.4"
+
+es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.11"
+
+es6-weak-map@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81"
+ dependencies:
+ d "^0.1.1"
+ es5-ext "^0.10.8"
+ es6-iterator "2"
+ es6-symbol "3"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@1.8.x:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018"
+ dependencies:
+ esprima "^2.7.1"
+ estraverse "^1.9.1"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.2.0"
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-config-airbnb-base@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-10.0.1.tgz#f17d4e52992c1d45d1b7713efbcd5ecd0e7e0506"
+
+eslint-import-resolver-node@^0.2.0:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz#5add8106e8c928db2cba232bcd9efa846e3da16c"
+ dependencies:
+ debug "^2.2.0"
+ object-assign "^4.0.1"
+ resolve "^1.1.6"
+
+eslint-import-resolver-webpack@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.1.tgz#c7f8b4d5bd3c5b489457e5728c5db1c4ffbac9aa"
+ dependencies:
+ array-find "^1.0.0"
+ debug "^2.2.0"
+ enhanced-resolve "~0.9.0"
+ find-root "^0.1.1"
+ has "^1.0.1"
+ interpret "^1.0.0"
+ is-absolute "^0.2.3"
+ lodash.get "^3.7.0"
+ node-libs-browser "^1.0.0"
+ resolve "^1.2.0"
+ semver "^5.3.0"
+
+eslint-module-utils@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz#a6f8c21d901358759cdc35dbac1982ae1ee58bce"
+ dependencies:
+ debug "2.2.0"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-filenames@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.1.0.tgz#bb925218ab25b1aad1c622cfa9cb8f43cc03a4ff"
+ dependencies:
+ lodash.camelcase "4.1.1"
+ lodash.kebabcase "4.0.1"
+ lodash.snakecase "4.0.1"
+
+eslint-plugin-import@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e"
+ dependencies:
+ builtin-modules "^1.1.1"
+ contains-path "^0.1.0"
+ debug "^2.2.0"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.2.0"
+ eslint-module-utils "^2.0.0"
+ has "^1.0.1"
+ lodash.cond "^4.3.0"
+ minimatch "^3.0.3"
+ pkg-up "^1.0.0"
+
+eslint-plugin-jasmine@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de"
+
+eslint@^3.10.1:
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2"
+ dependencies:
+ babel-code-frame "^6.16.0"
+ chalk "^1.1.3"
+ concat-stream "^1.4.6"
+ debug "^2.1.1"
+ doctrine "^1.2.2"
+ escope "^3.6.0"
+ espree "^3.4.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ glob "^7.0.3"
+ globals "^9.14.0"
+ ignore "^3.2.0"
+ imurmurhash "^0.1.4"
+ inquirer "^0.12.0"
+ is-my-json-valid "^2.10.0"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.5.1"
+ json-stable-stringify "^1.0.0"
+ levn "^0.3.0"
+ lodash "^4.0.0"
+ mkdirp "^0.5.0"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.1"
+ pluralize "^1.2.1"
+ progress "^1.1.8"
+ require-uncached "^1.0.2"
+ shelljs "^0.7.5"
+ strip-bom "^3.0.0"
+ strip-json-comments "~2.0.1"
+ table "^3.7.8"
+ text-table "~0.2.0"
+ user-home "^2.0.0"
+
+espree@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d"
+ dependencies:
+ acorn "4.0.4"
+ acorn-jsx "^3.0.0"
+
+esprima@2.7.x, esprima@^2.7.1:
+ version "2.7.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
+
+esprima@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esrecurse@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
+ dependencies:
+ estraverse "~4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^1.9.1:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
+
+estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+estraverse@~4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+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"
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.7"
+
+eventemitter3@1.x.x:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+eventsource@~0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232"
+ dependencies:
+ original ">=0.0.5"
+
+evp_bytestokey@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53"
+ dependencies:
+ create-hash "^1.1.1"
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
+expand-braces@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
+ dependencies:
+ array-slice "^0.2.3"
+ array-unique "^0.2.1"
+ braces "^0.1.2"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044"
+ dependencies:
+ is-number "^0.1.1"
+ repeat-string "^0.2.2"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+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:
+ accepts "~1.3.3"
+ array-flatten "1.1.1"
+ content-disposition "0.5.2"
+ content-type "~1.0.2"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "~2.2.0"
+ depd "~1.1.0"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.7.0"
+ finalhandler "0.5.1"
+ fresh "0.3.0"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.1"
+ path-to-regexp "0.1.7"
+ proxy-addr "~1.1.3"
+ qs "6.2.0"
+ range-parser "~1.2.0"
+ send "0.14.2"
+ serve-static "~1.11.2"
+ type-is "~1.6.14"
+ utils-merge "1.0.0"
+ vary "~1.1.0"
+
+extend@^3.0.0, extend@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extract-zip@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.5.0.tgz#92ccf6d81ef70a9fa4c1747114ccef6d8688a6c4"
+ dependencies:
+ concat-stream "1.5.0"
+ debug "0.7.4"
+ mkdirp "0.5.0"
+ yauzl "2.4.1"
+
+extsprintf@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+faye-websocket@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fd-slicer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+ dependencies:
+ pend "~1.2.0"
+
+figures@^1.3.5:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775"
+
+fileset@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+ dependencies:
+ 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"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+finalhandler@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7"
+ dependencies:
+ debug "~2.2.0"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ statuses "~1.3.0"
+ unpipe "~1.0.0"
+
+finalhandler@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd"
+ dependencies:
+ debug "~2.2.0"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-cache-dir@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+ dependencies:
+ commondir "^1.0.1"
+ mkdirp "^0.5.1"
+ pkg-dir "^1.0.0"
+
+find-root@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+flat-cache@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+for-in@^0.1.5:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
+
+for-own@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072"
+ dependencies:
+ for-in "^0.1.5"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+forwarded@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
+
+fresh@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
+
+fs-extra@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0:
+ version "1.0.17"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.17.tgz#8537f3f12272678765b4fd6528c0f1f66f8f4558"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.29"
+
+fstream-ignore@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+
+gauge@~2.7.1:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ supports-color "^0.2.0"
+ wide-align "^1.1.0"
+
+generate-function@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
+
+generate-object-property@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+ dependencies:
+ is-property "^1.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+getpass@^0.1.1:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^5.0.15:
+ version "5.0.15"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^9.0.0, globals@^9.14.0:
+ version "9.14.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+"graceful-readlink@>= 1.0.0":
+ 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"
+
+handlebars@^4.0.1, handlebars@^4.0.3:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
+ dependencies:
+ async "^1.4.0"
+ optimist "^0.6.1"
+ source-map "^0.4.4"
+ optionalDependencies:
+ uglify-js "^2.6"
+
+har-validator@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+ dependencies:
+ chalk "^1.1.1"
+ commander "^2.9.0"
+ is-my-json-valid "^2.12.4"
+ pinkie-promise "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-binary@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c"
+ dependencies:
+ isarray "0.0.1"
+
+has-cors@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+hash.js@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573"
+ dependencies:
+ inherits "^2.0.1"
+
+hasha@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
+ dependencies:
+ is-stream "^1.0.1"
+ pinkie-promise "^2.0.0"
+
+hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5"
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+html-entities@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
+
+http-deceiver@^1.2.4:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+
+http-errors@~1.5.0, http-errors@~1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750"
+ dependencies:
+ inherits "2.0.3"
+ setprototypeof "1.0.2"
+ statuses ">= 1.3.1 < 2"
+
+http-proxy-middleware@~0.17.1:
+ version "0.17.3"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.3.tgz#940382147149b856084f5534752d5b5a8168cd1d"
+ dependencies:
+ http-proxy "^1.16.2"
+ is-glob "^3.1.0"
+ lodash "^4.17.2"
+ micromatch "^2.3.11"
+
+http-proxy@^1.13.0, http-proxy@^1.16.2:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
+ dependencies:
+ eventemitter3 "1.x.x"
+ requires-port "1.x.x"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+
+iconv-lite@0.4.15:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+
+ignore@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+interpret@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+
+invariant@^2.2.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ipaddr.js@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4"
+
+is-absolute@^0.2.3:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb"
+ dependencies:
+ is-relative "^0.2.1"
+ is-windows "^0.2.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.0.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-extglob@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b"
+ dependencies:
+ generate-function "^2.0.0"
+ generate-object-property "^1.1.0"
+ jsonpointer "^4.0.0"
+ xtend "^4.0.0"
+
+is-number@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
+
+is-number@^2.0.2, is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-property@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+
+is-relative@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
+ dependencies:
+ is-unc-path "^0.1.1"
+
+is-resolvable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+ dependencies:
+ tryit "^1.0.1"
+
+is-stream@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-unc-path@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9"
+ dependencies:
+ unc-path-regex "^0.1.0"
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-windows@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
+
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isbinaryfile@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
+
+isexe@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-api@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.1.tgz#d36e2f1560d1a43ce304c4ff7338182de61c8f73"
+ dependencies:
+ async "^2.1.4"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.0.0"
+ istanbul-lib-hook "^1.0.0"
+ istanbul-lib-instrument "^1.3.0"
+ istanbul-lib-report "^1.0.0-alpha.3"
+ istanbul-lib-source-maps "^1.1.0"
+ istanbul-reports "^1.0.0"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
+istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212"
+
+istanbul-lib-hook@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.0.tgz#fc5367ee27f59268e8f060b0c7aaf051d9c425c5"
+ dependencies:
+ append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.13.0"
+ istanbul-lib-coverage "^1.0.0"
+ semver "^5.3.0"
+
+istanbul-lib-report@^1.0.0-alpha.3:
+ version "1.0.0-alpha.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0-alpha.3.tgz#32d5f6ec7f33ca3a602209e278b2e6ff143498af"
+ dependencies:
+ async "^1.4.2"
+ istanbul-lib-coverage "^1.0.0-alpha"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ rimraf "^2.4.3"
+ supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.0.tgz#9d429218f35b823560ea300a96ff0c3bbdab785f"
+ dependencies:
+ istanbul-lib-coverage "^1.0.0-alpha.0"
+ mkdirp "^0.5.1"
+ rimraf "^2.4.4"
+ source-map "^0.5.3"
+
+istanbul-reports@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.1.tgz#9a17176bc4a6cbebdae52b2f15961d52fa623fbc"
+ dependencies:
+ handlebars "^4.0.3"
+
+istanbul@^0.4.5:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b"
+ dependencies:
+ abbrev "1.0.x"
+ async "1.x"
+ escodegen "1.8.x"
+ esprima "2.7.x"
+ glob "^5.0.15"
+ handlebars "^4.0.1"
+ js-yaml "3.x"
+ mkdirp "0.5.x"
+ nopt "3.x"
+ once "1.x"
+ resolve "1.1.x"
+ supports-color "^3.1.0"
+ which "^1.1.1"
+ wordwrap "^1.0.0"
+
+jasmine-core@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297"
+
+jasmine-jquery@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/jasmine-jquery/-/jasmine-jquery-2.1.1.tgz#d4095e646944a26763235769ab018d9f30f0d47b"
+
+jodid25519@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967"
+ dependencies:
+ jsbn "~0.1.0"
+
+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@>=1.8.0, jquery@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
+
+js-cookie@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526"
+
+js-tokens@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
+
+js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^3.1.1"
+
+jsbn@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-loader@^0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json3@3.3.2, json3@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsonpointer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+
+jsprim@^1.2.2:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252"
+ dependencies:
+ extsprintf "1.0.2"
+ json-schema "0.2.3"
+ verror "1.3.6"
+
+karma-coverage-istanbul-reporter@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.0.tgz#5766263338adeb0026f7e4ac7a89a5f056c5642c"
+ dependencies:
+ istanbul-api "^1.1.1"
+
+karma-jasmine@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf"
+
+karma-mocha-reporter@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.2.tgz#876de9a287244e54a608591732a98e66611f6abe"
+ dependencies:
+ chalk "1.1.3"
+
+karma-phantomjs-launcher@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1"
+ dependencies:
+ lodash "^4.0.1"
+ phantomjs-prebuilt "^2.1.7"
+
+karma-sourcemap-loader@^0.3.7:
+ version "0.3.7"
+ resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8"
+ dependencies:
+ graceful-fs "^4.1.2"
+
+karma-webpack@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.2.tgz#bd38350af5645c9644090770939ebe7ce726f864"
+ dependencies:
+ async "~0.9.0"
+ loader-utils "^0.2.5"
+ lodash "^3.8.0"
+ source-map "^0.1.41"
+ webpack-dev-middleware "^1.0.11"
+
+karma@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-1.4.1.tgz#41981a71d54237606b0a3ea8c58c90773f41650e"
+ dependencies:
+ bluebird "^3.3.0"
+ body-parser "^1.12.4"
+ chokidar "^1.4.1"
+ colors "^1.1.0"
+ combine-lists "^1.0.0"
+ connect "^3.3.5"
+ core-js "^2.2.0"
+ di "^0.0.1"
+ dom-serialize "^2.2.0"
+ expand-braces "^0.1.1"
+ glob "^7.1.1"
+ graceful-fs "^4.1.2"
+ http-proxy "^1.13.0"
+ isbinaryfile "^3.0.0"
+ lodash "^3.8.0"
+ log4js "^0.6.31"
+ mime "^1.3.4"
+ minimatch "^3.0.0"
+ optimist "^0.6.1"
+ qjobs "^1.1.4"
+ range-parser "^1.2.0"
+ rimraf "^2.3.3"
+ safe-buffer "^5.0.1"
+ socket.io "1.7.2"
+ source-map "^0.5.3"
+ tmp "0.0.28"
+ useragent "^2.1.10"
+
+kew@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
+
+kind-of@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
+ dependencies:
+ is-buffer "^1.0.2"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+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.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:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+ object-assign "^4.0.1"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash._baseget@^3.0.0:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/lodash._baseget/-/lodash._baseget-3.7.2.tgz#1b6ae1d5facf3c25532350a13c1197cb8bb674f4"
+
+lodash._topath@^3.0.0:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/lodash._topath/-/lodash._topath-3.8.1.tgz#3ec5e2606014f4cb97f755fe6914edd8bfc00eac"
+ dependencies:
+ lodash.isarray "^3.0.0"
+
+lodash.camelcase@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.1.1.tgz#065b3ff08f0b7662f389934c46a5504c90e0b2d8"
+ dependencies:
+ lodash.capitalize "^4.0.0"
+ lodash.deburr "^4.0.0"
+ lodash.words "^4.0.0"
+
+lodash.capitalize@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
+
+lodash.cond@^4.3.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
+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@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
+ dependencies:
+ lodash._baseget "^3.0.0"
+ lodash._topath "^3.0.0"
+
+lodash.isarray@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
+lodash.kebabcase@4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.0.1.tgz#5e63bc9aa2a5562ff3b97ca7af2f803de1bcb90e"
+ dependencies:
+ lodash.deburr "^4.0.0"
+ lodash.words "^4.0.0"
+
+lodash.snakecase@4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.0.1.tgz#bd012e5d2f93f7b58b9303e9a7fbfd5db13d6281"
+ dependencies:
+ lodash.deburr "^4.0.0"
+ lodash.words "^4.0.0"
+
+lodash.words@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
+
+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.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"
+
+log4js@^0.6.31:
+ version "0.6.38"
+ resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
+ dependencies:
+ readable-stream "~1.0.2"
+ semver "~4.3.3"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+lru-cache@2.2.x:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+memory-fs@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+micromatch@^2.1.5, micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+"mime-db@>= 1.24.0 < 2", mime-db@~1.26.0:
+ version "1.26.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff"
+
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
+ version "2.1.14"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
+ dependencies:
+ mime-db "~1.26.0"
+
+mime@1.3.4, mime@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
+ dependencies:
+ brace-expansion "^1.0.0"
+
+minimist@0.0.8, minimist@~0.0.1:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+mkdirp@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
+ dependencies:
+ minimist "0.0.8"
+
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+moment@2.x:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
+
+mousetrap@^1.4.6:
+ version "1.4.6"
+ resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a"
+
+ms@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
+
+ms@0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
+
+mute-stream@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+
+nan@^2.0.0, nan@^2.3.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+node-libs-browser@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^1.4.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-libs-browser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^2.0.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4:
+ version "0.6.33"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz#640ac55198f6a925972e0c16c4ac26a034d5ecc9"
+ dependencies:
+ mkdirp "~0.5.1"
+ nopt "~3.0.6"
+ npmlog "^4.0.1"
+ rc "~1.1.6"
+ request "^2.79.0"
+ rimraf "~2.5.4"
+ semver "~5.3.0"
+ tar "~2.2.1"
+ tar-pack "~3.3.0"
+
+node-zopfli@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/node-zopfli/-/node-zopfli-2.0.2.tgz#a7a473ae92aaea85d4c68d45bbf2c944c46116b8"
+ dependencies:
+ commander "^2.8.1"
+ defaults "^1.0.2"
+ nan "^2.0.0"
+ node-pre-gyp "^0.6.4"
+
+nopt@3.x, nopt@~3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ dependencies:
+ abbrev "1"
+
+normalize-package-data@^2.3.2:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
+
+npmlog@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.1"
+ set-blocking "~2.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-component@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+obuf@^1.0.0, obuf@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+
+once@1.x, once@^1.3.0, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+once@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
+ dependencies:
+ wrappy "1"
+
+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"
+ dependencies:
+ object-assign "^4.0.1"
+ pinkie-promise "^2.0.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+options@>=0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
+
+original@>=0.0.5:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
+ dependencies:
+ url-parse "1.0.x"
+
+os-browserify@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+pako@~0.2.0:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+
+parse-asn1@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.0.0.tgz#35060f6d5015d37628c770f4e091a0b5a278bc23"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parsejson@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseqs@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseuri@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
+ dependencies:
+ better-assert "~1.0.0"
+
+parseurl@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693"
+ dependencies:
+ create-hmac "^1.1.2"
+
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
+phantomjs-prebuilt@^2.1.7:
+ version "2.1.14"
+ resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0"
+ dependencies:
+ es6-promise "~4.0.3"
+ extract-zip "~1.5.0"
+ fs-extra "~1.0.0"
+ hasha "~2.2.0"
+ kew "~0.7.0"
+ progress "~1.1.8"
+ request "~2.79.0"
+ request-progress "~2.0.1"
+ which "~1.2.10"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pikaday@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
+ optionalDependencies:
+ moment "2.x"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pkg-up@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26"
+ dependencies:
+ find-up "^1.0.0"
+
+pluralize@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+
+portfinder@^1.0.9:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+private@^0.1.6:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process@^0.11.0, process@~0.11.0:
+ version "0.11.9"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1"
+
+progress@^1.1.8, progress@~1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
+proxy-addr@~1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074"
+ dependencies:
+ forwarded "~0.1.0"
+ ipaddr.js "1.2.0"
+
+prr@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+qjobs@^1.1.4:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
+
+qs@6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
+
+qs@6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
+
+qs@~6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+querystringify@0.0.x:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c"
+
+randomatic@^1.1.3:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"
+ dependencies:
+ is-number "^2.0.2"
+ kind-of "^3.0.2"
+
+randombytes@^2.0.0, randombytes@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec"
+
+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"
+ dependencies:
+ bytes "2.4.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"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~1.0.4"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.0, readable-stream@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
+ dependencies:
+ buffer-shims "^1.0.0"
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
+readable-stream@~1.0.2:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@~2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
+readable-stream@~2.1.4:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
+ dependencies:
+ buffer-shims "^1.0.0"
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+readline2@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ mute-stream "0.0.5"
+
+rechoir@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+ dependencies:
+ resolve "^1.1.6"
+
+regenerate@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
+
+regenerator-runtime@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb"
+
+regenerator-transform@0.9.8:
+ version "0.9.8"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+ is-primitive "^2.0.0"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request-progress@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08"
+ dependencies:
+ throttleit "^1.0.0"
+
+request@^2.79.0, request@~2.79.0:
+ version "2.79.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.11.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~2.0.6"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ qs "~6.3.0"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "~0.4.1"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-uncached@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+requires-port@1.0.x, requires-port@1.x.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@1.1.x:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.1.6, resolve@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
+
+run-async@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+ dependencies:
+ once "^1.3.0"
+
+rx-lite@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+
+safe-buffer@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+
+select2@3.5.2-browserify:
+ version "3.5.2-browserify"
+ resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
+semver@~4.3.3:
+ version "4.3.6"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
+
+send@0.14.2:
+ version "0.14.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef"
+ dependencies:
+ debug "~2.2.0"
+ depd "~1.1.0"
+ destroy "~1.0.4"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.7.0"
+ fresh "0.3.0"
+ http-errors "~1.5.1"
+ mime "1.3.4"
+ ms "0.7.2"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.3.1"
+
+serve-index@^1.7.2:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b"
+ dependencies:
+ accepts "~1.3.3"
+ batch "0.5.3"
+ debug "~2.2.0"
+ escape-html "~1.0.3"
+ http-errors "~1.5.0"
+ mime-types "~2.1.11"
+ parseurl "~1.3.1"
+
+serve-static@~1.11.2:
+ version "1.11.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7"
+ dependencies:
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ parseurl "~1.3.1"
+ send "0.14.2"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+setprototypeof@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
+
+sha.js@^2.3.6:
+ version "2.4.8"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f"
+ dependencies:
+ inherits "^2.0.1"
+
+shelljs@^0.7.5:
+ version "0.7.6"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
+signal-exit@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+socket.io-adapter@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b"
+ dependencies:
+ debug "2.3.3"
+ socket.io-parser "2.3.1"
+
+socket.io-client@1.7.2:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.2.tgz#39fdb0c3dd450e321b7e40cfd83612ec533dd644"
+ dependencies:
+ backo2 "1.0.2"
+ component-bind "1.0.0"
+ component-emitter "1.2.1"
+ debug "2.3.3"
+ engine.io-client "1.8.2"
+ has-binary "0.1.7"
+ indexof "0.0.1"
+ object-component "0.0.3"
+ parseuri "0.0.5"
+ socket.io-parser "2.3.1"
+ to-array "0.1.4"
+
+socket.io-parser@2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0"
+ dependencies:
+ component-emitter "1.1.2"
+ debug "2.2.0"
+ isarray "0.0.1"
+ json3 "3.3.2"
+
+socket.io@1.7.2:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.2.tgz#83bbbdf2e79263b378900da403e7843e05dc3b71"
+ dependencies:
+ debug "2.3.3"
+ engine.io "1.8.2"
+ has-binary "0.1.7"
+ object-assign "4.1.0"
+ socket.io-adapter "0.5.0"
+ socket.io-client "1.7.2"
+ socket.io-parser "2.3.1"
+
+sockjs-client@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.1.tgz#284843e9a9784d7c474b1571b3240fca9dda4bb0"
+ dependencies:
+ debug "^2.2.0"
+ eventsource "~0.1.6"
+ faye-websocket "~0.11.0"
+ inherits "^2.0.1"
+ json3 "^3.3.2"
+ url-parse "^1.1.1"
+
+sockjs@0.3.18:
+ version "0.3.18"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207"
+ dependencies:
+ faye-websocket "^0.10.0"
+ uuid "^2.0.2"
+
+source-list-map@~0.1.7:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
+
+source-map-support@^0.4.2:
+ version "0.4.11"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.11.tgz#647f939978b38535909530885303daf23279f322"
+ dependencies:
+ source-map "^0.5.3"
+
+source-map@^0.1.41:
+ version "0.1.43"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1, source-map@~0.5.3:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
+source-map@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
+ dependencies:
+ amdefine ">=0.0.4"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+spdy-transport@^2.0.15:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.18.tgz#43fc9c56be2cccc12bb3e2754aa971154e836ea6"
+ dependencies:
+ debug "^2.2.0"
+ hpack.js "^2.1.6"
+ obuf "^1.1.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.4.0"
+
+spdy@^3.4.1:
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.4.tgz#e0406407ca90ff01b553eb013505442649f5a819"
+ dependencies:
+ debug "^2.2.0"
+ handle-thing "^1.2.4"
+ http-deceiver "^1.2.4"
+ select-hose "^2.0.0"
+ spdy-transport "^2.0.15"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jodid25519 "^1.0.0"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stats-webpack-plugin@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.4.3.tgz#b2f618202f28dd04ab47d7ecf54ab846137b7aea"
+
+"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-http@^2.3.1:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.1.0"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^3.0.0"
+
+string_decoder@^0.10.25, string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-json-comments@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+ dependencies:
+ has-flag "^1.0.0"
+
+table@^3.7.8:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+tapable@^0.1.8:
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
+
+tapable@^0.2.5, tapable@~0.2.5:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"
+
+tar-pack@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae"
+ dependencies:
+ debug "~2.2.0"
+ fstream "~1.0.10"
+ fstream-ignore "~1.0.5"
+ once "~1.3.3"
+ readable-stream "~2.1.4"
+ rimraf "~2.5.1"
+ tar "~2.2.1"
+ uid-number "~0.0.6"
+
+tar@~2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+test-exclude@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.0.0.tgz#0ddc0100b8ae7e88b34eb4fd98a907e961991900"
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+throttleit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+timeago.js@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
+
+timers-browserify@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
+ dependencies:
+ process "~0.11.0"
+
+timers-browserify@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86"
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@0.0.28, tmp@0.0.x:
+ version "0.0.28"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+to-array@0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
+
+tough-cookie@~2.3.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
+ dependencies:
+ punycode "^1.4.1"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tryit@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@~0.4.1:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-is@~1.6.14:
+ version "1.6.14"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.13"
+
+typedarray@^0.0.6, typedarray@~0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uglify-js@^2.6, uglify-js@^2.7.5:
+ version "2.7.5"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"
+ dependencies:
+ async "~0.2.6"
+ source-map "~0.5.1"
+ uglify-to-browserify "~1.0.0"
+ yargs "~3.10.0"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uid-number@~0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+ultron@1.0.x:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+
+unc-path-regex@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
+
+underscore@^1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+url-parse@1.0.x:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
+ dependencies:
+ querystringify "0.0.x"
+ requires-port "1.0.x"
+
+url-parse@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
+ dependencies:
+ querystringify "0.0.x"
+ requires-port "1.0.x"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+user-home@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+ dependencies:
+ os-homedir "^1.0.0"
+
+useragent@^2.1.10:
+ version "2.1.12"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.12.tgz#aa7da6cdc48bdc37ba86790871a7321d64edbaa2"
+ dependencies:
+ lru-cache "2.2.x"
+ tmp "0.0.x"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+utils-merge@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
+
+uuid@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
+
+uuid@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vary@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
+
+verror@1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
+ dependencies:
+ extsprintf "1.0.2"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+void-elements@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+
+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.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"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.2.1.tgz#01efa80c5c29e5c56ba55d6f5470a35b6402f0b2"
+ dependencies:
+ async "^2.1.2"
+ chokidar "^1.4.3"
+ graceful-fs "^4.1.2"
+
+wbuf@^1.1.0, wbuf@^1.4.0:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
+ 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"
+ dependencies:
+ memory-fs "~0.4.1"
+ mime "^1.3.4"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+
+webpack-dev-server@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.3.0.tgz#0437704bbd4d941a6e4c061eb3cc232ed7d06101"
+ dependencies:
+ ansi-html "0.0.7"
+ chokidar "^1.6.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ express "^4.13.3"
+ html-entities "^1.2.0"
+ http-proxy-middleware "~0.17.1"
+ opn "4.0.2"
+ portfinder "^1.0.9"
+ serve-index "^1.7.2"
+ sockjs "0.3.18"
+ sockjs-client "1.1.1"
+ spdy "^3.4.1"
+ strip-ansi "^3.0.0"
+ supports-color "^3.1.1"
+ webpack-dev-middleware "^1.9.0"
+ yargs "^6.0.0"
+
+webpack-sources@^0.1.0, webpack-sources@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.4.tgz#ccc2c817e08e5fa393239412690bb481821393cd"
+ dependencies:
+ source-list-map "~0.1.7"
+ source-map "~0.5.3"
+
+webpack@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.2.1.tgz#7bb1d72ae2087dd1a4af526afec15eed17dda475"
+ dependencies:
+ acorn "^4.0.4"
+ acorn-dynamic-import "^2.0.0"
+ ajv "^4.7.0"
+ ajv-keywords "^1.1.1"
+ async "^2.1.2"
+ enhanced-resolve "^3.0.0"
+ interpret "^1.0.0"
+ json-loader "^0.5.4"
+ loader-runner "^2.3.0"
+ loader-utils "^0.2.16"
+ memory-fs "~0.4.1"
+ mkdirp "~0.5.0"
+ node-libs-browser "^2.0.0"
+ source-map "^0.5.3"
+ supports-color "^3.1.0"
+ tapable "~0.2.5"
+ uglify-js "^2.7.5"
+ watchpack "^1.2.0"
+ webpack-sources "^0.1.4"
+ yargs "^6.0.0"
+
+websocket-driver@>=0.5.1:
+ version "0.6.5"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
+ dependencies:
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which@^1.1.1, which@~1.2.10:
+ version "1.2.12"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
+ dependencies:
+ isexe "^1.1.1"
+
+wide-align@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad"
+ dependencies:
+ string-width "^1.0.1"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@^1.0.0, wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018"
+ dependencies:
+ options ">=0.0.5"
+ ultron "1.0.x"
+
+wtf-8@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
+
+xmlhttprequest-ssl@1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
+
+xtend@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yargs-parser@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs@^6.0.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^4.2.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"
+
+yauzl@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+ dependencies:
+ fd-slicer "~1.0.1"
+
+yeast@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"