From f5e8d6bded71a0ed827c2f1becb3a3a81ad1f163 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 25 Apr 2017 17:15:41 +0100 Subject: [PATCH 01/22] updated to new favicons --- .../dev/favicon_status_canceled.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_created.ico | Bin 0 -> 4286 bytes .../ci_favicons/dev/favicon_status_failed.ico | Bin 0 -> 4286 bytes .../ci_favicons/dev/favicon_status_manual.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_pending.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_running.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_skipped.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_success.ico | Bin 0 -> 4286 bytes .../dev/favicon_status_warning.ico | Bin 0 -> 4286 bytes .../ci_favicons/favicon_status_canceled.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_created.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_failed.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_manual.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_pending.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_running.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_skipped.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_success.ico | Bin 5430 -> 4286 bytes .../ci_favicons/favicon_status_warning.ico | Bin 5430 -> 4286 bytes app/serializers/status_entity.rb | 5 ++++- 19 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_canceled.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_created.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_failed.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_manual.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_pending.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_running.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_skipped.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_success.ico create mode 100644 app/assets/images/ci_favicons/dev/favicon_status_warning.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_canceled.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_created.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_failed.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_manual.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_pending.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_running.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_skipped.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_success.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_warning.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico new file mode 100644 index 0000000000000000000000000000000000000000..4af3582b60d2fa40201791f2865491fb84e0ae76 GIT binary patch literal 4286 zcmb_fYfM~K5WY<{8l$Pk#z$hc{<+VEZi8qDO}ZFM(%MGK?qX?wD5W&Men6EU2(eUa zV%w~VA;i)))Ln|DBsF3}h)EOsTO0U6tnsmSVS#Lmr9dd$ecf@seRa89c7gV6hI7xH znfd0--e{Mvw*)s&y#}ij35Xy6SD*d6Znb2dk=cJ-R|{@qU;hyu|t;SVg8FKcZ>ciw_zX_84zBV_atpve|6$nwlDFZf>UI$B$EeeLaveT?Mn0_celQqJ ze?CBt)pne=mlH*srTeW%w}e0%%&3S)nG#bcNA(l7ry_zy?DIa_SeW?AWn%EhlEwnpM8Dm2g+% zz2JK4$j0}3x4tpft^GN9Nu$v&g@WIywBV5`{*_3vlOk0XNmRaL2gT#@j5+ip>JqdP zY$)QrfIP>47x1~p9=_o#_-wpe)kEK_H0o1m#HZ5iH%bOO`ng1XJEVkOZ*Ok~KO7Dd zd_WxXof}GcFYsn@?On$^_D^T=reK#gFxkWL<})X-dA{AQCb3WO*-wcS`B*Ko`~N#kbE0dIi^cg&r(RK2CKF(dYemg1*<`RVm! zU%(G5dc`hN^-g&hZ+A9k_V*uMcE{9N&4_cK1202-cH>K#sG7+o5xFf0{CzTonk5?e zP$b{GBAq@U(&ajlemx@6WQRt7abIWSnZGAm_dKQ_QvNn-Wr`l;T&E~rCzD>Q{H#}~ zJ9SqM!hWsV5Ban%W}{Yu{*7C$j>pZI=Cxj-wBvl}u{7b#^A)x#v#iy6{w>>Ha;5T= zvH^SgUgeCHyqTxFY~=>I~Dd7|JS2Cm2*_ZF@!$P3r_8w zTzKZa1-G{Uo_@lAuURJ7`mR&kgB+w{KYhD$`JS<|Gg(>mJfHbC^pX2{m#xxDZaLoc z{(`5XmC`u9EmPR3^e-oui_bax5&BV=9Gc*o$R%sF$GBZot&r~24qNT#<1M(<8tAi~ zo_w-a`}0nuz_^p5vKJtEv%zJf&{uM+#~kIV2UwKGXdt%C3p({|9+ Uqp2SSjs__C8=+~M$^63fKe{^4Q2+n{ literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico new file mode 100644 index 0000000000000000000000000000000000000000..13639da2e8a343576006037c7ca2345beee1fd20 GIT binary patch literal 4286 zcmb_fZERCj7`_CJ#%Rz;{76Lc&)R$2!3>%F7zz7fOhAZZ?Iy*jVM7xmKNRE##>k>E z5y*&!5E(2$*G+~AhQtsfU?dUt;fA_^9sB6$+I8txx0Qyqx1X=?b9Xzgx3}#koaXK6 zIp=-f=lwh<6@(@HS-Dccx6uB$AUq`qf|-f8gi0ptD}&GO_aLIBSFT*yc=hVlCZ_95 z)4Y~>9%Z`3w5zwb_xbyc3w<3O9h+FcoAIc#von3;#tj-88lsVr5xRNvCSALBErm1Y zN%DNSySw|z`(X!sW}S`D-P6;Pnw*@ZbUMx3xg#2lDmF2HhRM%1tiD$pSWgSEhlhtL zlgZ?Q8tg7Y=<3-N4fhzu#XF8#1s@-zT^q7=N@Fes9y7WxiJH5%-U~6&!s&`}_Ng;==~i zq;}wr;(fUgzUO3Fd7#bO6Z+mlBWFHPaN_Z}RtEg}zt7gg(HV#lE|FxpeqM`3k?w@>8gt;@Xv+oB>D6!j2sVyb+*V$I;WWS7bD@GLg!kdL!;n-|~?;dCAuE<%df}6z-9+N1! z-$W^3gE;nyx;gyWx(e$0Eo76n)@8No+tJ_ z^QD={!DKfP)ff==x0*>lxRxeQI4IzHpL}f}Qt;Gv3Lo7>)6J`Bko#JTr~f_Pu=x>r zhd7j}H&e2XbFEVPUDjJK{w!BX8)SO{!g;-P9{gEZ%$a%<kvYS8F_DFQKH(^v@l~+Nbrr zM(c=U&2r|SfZV(e);C*Yea(npzZa^jcr0Nvdr6fi?zgyI{6WuGXq~W$XCMcB=wW-t zW~#~-{I5s0iOp2YF@!wNF`MO`LU{VKSdC@dU3tZRr=F+Kde&yyj2x)34^LIE%G)zW zdP3D}o@Jk>AdlQf?bd1|zI=P(ZKY2nt4#rV!%T6Tcs?Irp?%KTImjpN=HNKjL?K?I zGsbn1LnPT|dC%y)*j~&oZHGMTX)nfWbRM-ywQ0L49eE%_}l(JN6=oecVdBkOC+qPjIfMk;UT7_6c(0{S0RBe3wMca$uOmu p!c1PK1|}Y`6Ym*lS%V;ZheMTuvx92}``WJpq)k6RP literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico new file mode 100644 index 0000000000000000000000000000000000000000..5f0e711b104b2e4bf88a3c726cdb9b7f17a09844 GIT binary patch literal 4286 zcmb_fdu&r>6u$(G#%Rz;d?cdK;o9DIJc5hSwWA0zK?iK>-C&p~Zh)GgW)oC28AGCB zkP!$$8L!!m$?ydU8HR*J{x$eYPzC|u&xZ|-RV(LK~r_b?w4j+T+MkF*M&T* zYe;8x$vk)KEUvRUi?dE=aX)#daiNd(mgHHT7iSeYNx#S)d$qQYKHT0}ASZ98E^Ics zi*3I8Hs`f^^z=o@V)xLNU4aSK^Z;52#Jt50k}zqx#?%(C6}``yO@x7krRmkxQ9@4L@iN+z;%} z?p*jkH!qmhbJV=Av94axIXn{6Qm3rdZuS4q2KXV(!hR^I^SKR;Qw-AOs$HX zU1258z=Y3J{gBB9+|}q8YWzQ23LoofGau_eW+HF9k$i_l3h&U9Z`*SU{+=U4Y4}G^ z3@g0Ucqj7#brDjf4DM?53)FKezS}W>iMQQ!DRA6GJ#9wn{Zgc{CIiK`=Tme?J}qB- znqFAhMKO&OfDQ0N>Ofue`qD3G^b7b3eG~YcV|RD87S#8(i?aVKk$PH<)ZHr5$R6Va zcH|R1ohVta$aS0=PKCLt_b{7-o`t@NKIS@7IHR}C^h2P7{mo}quz4QbCyrw`^4WLh zQ}{gtMa%MOh3zyIEXiDlxr{;2QqIo38)tVr3hG9_Gb^}BZ2tj~B8_^A0vp7>4_A-l zdrFIGe(`T)DR*bQ3o_Y&o|V9E0yc)bb*{zRCdPW%=ZLxYh)AKGdWxlBN4DuHQpxt% z*%qer%aLpRz9TKfPR6V1#I+g_m0G3AU=Ei zQv*>IlcXoA&>*a@GLXOidFrfPLoILC(dX+vrf)X9ML&H|MuFxzbeZcq6;J){Ygl%l zyw>Q9*BB^L&9P2UY@LDR8sizcR9q@cSqS_Zu@n1~vY6vFddPPTG}!J}W2(R9QsX|H z4>gt~oO!g=T&dox^*y@6Tt-qNP6-{*CzlxyXzf?8$&&fSIPeoW9+&bdV%Pt3t?JoF z)qAzRN2(S~=l%VVo6y1fu2+~}*4nRL3se_95VjjUl#nN`w~%D~UcE2dci3(`4msdM z58LCtS~_pizj|c5@eozA44_jS8z8Dfy|eQ>aR zPR5$i(i13O@Hn4&5b}uqsAMkJ+Lvi9xVqq>NV&d`RvIW|H+E*)mu;P6b`tUt$>8^K zPGsAw#bewysxp#nH?7y==UN++#MdFudOC9L)#8uZ#mbnZ4+Sgc%+liLS{q$eI3p_Q zWwzs$Tzj?nBUQHP{>q|{2tV6zarmX8&HaThrJyi{JPHYPPPjpA tOPndn6lC%+H8Amjo#%*_*Yr)5D4)?qF zo^yWZd2e8tMfh93oT0yr^JRv4jbRuL3AY#{6799&)LG^(ChaRObj7H-#h<&f2p&>=zNE3{Ub+?fY<9yh{-eE4|GS(hkvPEzhvaJ_V9sEWByD{ zO(peEjm6_A&lfLVOrFt4adC0-oN7`z=#HXV&W7(gr8A5{`Fv1P zQj(0dGS7iP01O6062GOTMF}t8lj#3sCj9^GIrZbGg(IHMTbD0iPSHaP ziaW(2FE0-q4hIYj3_xXNWfD6*E+j*DHP*QYrv#up=iSfh(sbvb zV~ss~t0Sj*%ppkLp9L7{;9bbkSJ4GKY5_MTxS}N=A?d_G{ zhV1k7v9MC7 zOX)+UF%uIL^4**1U7|OOe8=3Eknb*g&d%wxS{XNi?L8(yXdeq1>Ghz*q;GmJ=*=Q*jC%*gJ%-LO1E-A7yY1qQamu@0fcxLa>n(v#vELQ* zJ%LX%#es4`I;ke9FQ_-^exO>Qdy?*IdM^ZlkJ4R(G5<1wtDN2T92?y+_uAt1mcXW& zVqY^CKVeI6VXQC64mHZtaZpjrU8A>KiK+blvr}iBJT68f*yn_oB=kM{EeBA6WMKiU zDn$F6Iq){I&|jy4j!F$2-Kv4JwHo++rv`lOBK(bgt;9?J9&asuL8{^ZiB@wE+J?D~ zQ>2yysha;yDiew&OBSN@YN3z#6S6c%t67r2b*I(%VmfB}S}NlYlYi7>2_m0o%CzO_ zylU&2kF=Y>63G!Qbg6Zb3Le?6U7Z9QF~UyA&< zNiL}a^*yj^KUCvS-}7zDe<^6@+z^)s?6+A9|4TYwwsp|Vcaa?7lOD9kbFWOd;Qx9g zGk*fgF@_|MbHFTqk_|8YEMOJ4Jd~I1x2N-DTR$+1rIdqs?EQC5nR`Y}kI$rk6Meo* z@|61-i`JyZmub(xCFhlpi5-KFIhZx`eVO>O?PJbiiCsg hr1y+yNh`y=gS3uluqFNjcZ(ZhzhUshRPr|q{{t;Pg#`cr literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dfefd4cc5a284aa507915ee8dd8efb3ab2a34b4 GIT binary patch literal 4286 zcmb_fdu$YC5MP4jsDa=EMU5yXq<6b_Eof6QX${pF6Hv76Z3_n`#uj{_e+b$?G(;MR zK?(hhU_Nb-Z-hR8|%-(K$x!YbJ^t()V z_nZ0VH?MCd<+yqHTd{(pzg)vz9Cts*aW*8};?j|5Ut7qV>y>>1=37eG`nVESVR6f? z3*BsBftyJSOIb&pn>ATVm7Vbw@W@=_l0Hirdl4}&#g)jgsEjG=zE-;ob(}!^drn6n&E7~6U zYPPhyiTz=^_+sU$GQz(+5UuTOM+l$zvJSWLOb9=($sC{h1NEq>@PDgFUea0TaPK-= zr1>)_%?&>FD5~XH_-)mzQ+nzhzxJK5Ls7{d4ZqtYpR@6w`Xu%0$?*U7B)-#IC*1Tm z2ymlZfRhKeYxw8GYRrhlZ1Aa9)4Yhp|D!tPfzDdT@$1L!&|S|%?@1)7vqt6;PKKtE5 z2zx*JejovY@7aK5CcsnsF2X(KLDPE}8#%YXzkXV@^5mt`cDEfEAQ5 zYifZ<}j z-VA(-eSXMy1D~E02g(KMq?+7wK>@m()E}rvQJwH^Rl$2s zxgICr-e{coDVGrUC>vV1b^YZ4#$gp z?=9GHr?i#7s^-~1&c$5oq-?W+l*gZyGK6)K7=wtPC!8jE<19VZJS&}Vzfq9BC=xUB zTgu?w=Dd^D5!%HLi*YAUm!9 z4!hPYiEYAn1+-e|Uc^Tc`0eov%KtnXV+`v%H^ z9{WIlR??(*#!OFNR`P@Bb3dJ@+>eNkEHk;u_5zy|@0PQyJ+RRRV^03`WOA|gF=waf zye!)My;u{mWX*VpTMI58B&Yp#GyYV2qoVK{oku+lQ^}g~N1Q^AB3j1+*-4L?@u%7w z*_5)JiB<{i*fN!@8Gp!?zSN(S`XSK2?Xz5dG4+G%)4aC;+*(k%B_MGYr1=ow=7CQm z4$g5Cgtn+i3@L!*Ln=VRMRw9WBU)0xagQRcB|5iT|AB9}4?@2V2*Azp-x&N4kZ+6h literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico new file mode 100644 index 0000000000000000000000000000000000000000..a41539c0e3e8fe178f3ea88fbce6c66b00df3c2c GIT binary patch literal 4286 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico new file mode 100644 index 0000000000000000000000000000000000000000..70f0ca61eca731f254935c0babc2d4002b0c7518 GIT binary patch literal 4286 zcmb_fe@qj16hFh3Ez1_S%F`9EX(GP3~BEGNpuLhWSZ#wF{eu= z*SSyHFj{dl}_pU!I?a|gBcX{01 z`@Zk{ykFn`*$-<2V%HLO&2L-5CwY}%V|ZPzKVa2~EFZ>e!l9mTasW}qIZ zCX+uG1JgJ;Si=hs99R!ok(SOJTm!3)Lj$P&lsoAlVr3mehH) z=jXnYF)9gr>*+M+Bn7)s6JMMIk!;i!+Je~?=CuYfZB(j0} z1NEpx_#M^RlY5UCs?0}c$akwSDp9=Gnv4Q#|_3DZ6ubMO0I*zcn`x{uev7d$WmFcoi zU9R85IPqR0ZCKYfKSq>#HO-4?{L6=PUh1tg92+>sfxTW2j)N?Cc4@)6cczTr)cIwM zu8A0A!^+LkO8hgB_HuIC~^{m)`l!e|}JzT40VYosc#`b=yg`YMC zYM{NR@qyuKUeL@6%uPP!-$OZ?zB;bW*?{pzpJi-(x~kX!_D=MDcLsPi>cF=&13ucX zffdaeG3PK@gJ@=v?=-u~_il6MhT$fIj2pu4Kfr=_rxtvKO@y)ctPJ2gmd}KedJU}o zcFujx2a&AAw!q{#t_#?2rpJ~A>pgl&k1*voO4A};X zLwip{!g`>2K{G3ejkB9i37|7f`{CRtZDy|1L3uZ`FtRsTZy|h&{pNu0A$)pL94Hs0 zlWLOA1)WXm57eWmPg1X@d4au&{`U|C>(osl+{mZRoVka3%wT1(-a^=+*q6oPhio~! zJ{`6kOby1BVmqEzSTyi)!?a5@yCX3p->=kV<=(2{BtM>W%5|8~cWH+Xpa{vN1>hBk z_=P%fp{?hu(xG)<8Z=j@!H-|1!R3lq;ARa6|KPcf#EX7+lrMQoEYo*Mr8@Ag#9Rl- zze)#Uss1}Lk9}J-#UWZRWm`!;w3nVzsh0Nl+$hg|IvO+jTg=l}k$-f?LPS2h^9(|? ztkSpp9m7&E1@jctLHfj{`U6Vx(LB*)cvB+$V2w+r4DcGYH%E%*ycjL3^gUdZJq6_l zXkSbuJu7AeU`^PDT^KI1X!Y!KWJc;3cL`sQf4c;6nQzLEA3KIuVw?1On(N&o5* zjrxNiU<_$LK1Yq*ig;`epZ^&dLo{?Fh}DvX+^Z7IFYQ<`fyS1 zlzw6EHlUyF4|w&P=58HG^4tP23qWEfgUCFAlmbJ{B(TZEz!~Nqp)C@U4`~R=hE$G( j7uiYkjA(K>!%Rb3Ky+q%@CMs<8wBoaFa)>5_Za*Ssj5h) literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico new file mode 100644 index 0000000000000000000000000000000000000000..db289e03eb15e0b5d799c1e58864e5941ca66da0 GIT binary patch literal 4286 zcmb_fYitx%6rNHH#z;WLR{|Cj?e5HOAEq|`C`x10M3mTew+n?INDCweV?q@Z{2^5$ zfj*#HXrzFJ-EE;Xk>C#`CKW@k&C&Q7<`GdX*A z&OP^h=W*{nyMmBF&+63ze+muD1>p%n5Uj)`60(U|t`f~HI5i?saATct7&Qr%Xj$aO zFpV;e9YQ6pki3}YpILBRHdu+b39FSbu%rs%*Utk*-vb{s0j`U{bEgsFInoJ|?h)ZA z9$OGUA~9F6?WC^|^0fiuAsABIFtGJI)dz)g(j`AEzXD8#4Pj>qf9eGu zuR(}%F?L@*RAM({?`fU|p$h8%0QUY2@Te#MXv7CK5BE}wDy-zaweA8GMQ>btPjmz9 zACE@Fc_15#x*z6S2-@RhH+(Z-WH}-u+bTYfu}Iqw!FipFt603K5+fVFjb$|=KX47$ zav9(;7V)0s98nH-#hn5-*6^CMH)2_xpIkkgd_TlVxyQcwlJQ%I%y9}X0cmvrk7mU8 zgL+5Jf&Xhw#jd-a~l>1hJ`q*E!nX9^dwo8Kzly8bz z#intnT8dyFq}a=nG4-Jp!Q5oLxbHfa9u3DlhoUzU8;?fBJxleGdN;@3?nx;g{o1bL zMzQ;jO9&pcAjH_rl>4bdD?)^SbMvcsv^;>MlcAVC7~yisxMyj3uV8GZc5m96Zl4tH zqMUQhyK5y(mRS&JNTCh#tliYLpZ1VlUcNE`~d{=Y7SX`wj z>Rsc;M^W6Fqu-NK!FSBa5pBOkv7@=)HXEOP2ol%r*rlEeKAT#6{~MRPZ{wBf<`(+R zv2rHn%_#Xe{3ecll@7UtWgV z@{jRDSpf!4q~cFH*E8|--+N2eKO}DxZz@H!#|x>}8i%)AAs304t(Xj{)WQ%R8wZf$@R7TGgk1@lX_jF_tAok6{O$K>!xjxy*v5#w~X}lvcbZvrBe>8 zAKH4*d7E^K=kTdai+%`@m3MYRofZWM?oh<7tX{h}U!N$DQ^(BfVI%z^0T(f_au6 zY_MX|AzqB7XC|kbUEuXWr?sz__JowUY6PFMZv7g+}n3t6ZLK5*>=BB;c1OHw> NB2NbbxIO($;y(%}fBgUe literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico old mode 100755 new mode 100644 index 5a19458f2a2e0e5585f3932f01736cf838b86c76..23adcffff5082fb84b1dc17f547875d7d3792251 GIT binary patch literal 4286 zcmb`IUrbwd6vr>Y#K)N!eKJb2aqsW8jG=KRn3#eS*jU}%SW(AfhDvc|2gCFM6QU1G#1|7@K(lNQCM49ZT^$Im-CSGl?eTms+~)T7mbM$e=JfZw z=l473bAIRdPm;>`S5YD1U;6F|NqSn6B*DTesg4DDqj2AD7oX2puBvLYEX(6Uh#TBq zR}>||b%5*ZtpC{Uc0aq@T=-+2ml?O1K~+^%dV702`Tc$x92}&st}be9Y}C-l<2Jb- z<@(9phy!zGUqkR+Uth1y&dyRIksznCyu3`ky}iV7rr9RQxj68sF|e;Gu>1P@D3wY% zVRLj#OG{K+TdQ#l_Z}q%X7_+SG&GdMD5MC5LS_uXhgpm2>gs2hpXli5NEc$6{lQ?+ zY#KH4fVIGWD0Lm!W}Nr69tZ@od^&xk(`oAI=^^B5Vqzj|2=)Wdf3uWxQCC;@7_&FJ zySuY|Iel2!;6to=z@FUJcNd$3y)V4{Ds1NF=A4ijof8{*uos+7UWe<&@b`aC9=Y?{ z1Mm@hzLJj(y}jUmz#UbHFNXDz%1J#fw~rY)i^XDgSH^cKY#88gP;Q;p}&wt&C@A`s#$~RilU{5i-m!HXPut z<}+}^&R2%DYw{H58}o)-U^&Oq5jOavqoY~;fq?--zrCN0Gv~D4RwLZicrVyy+~n{5 zlJ+TM+MNs+>`U8h@L81VW7N>lNV=|P+~GWy!iFK<3)pb}o52?s>0IT6re(3OFtf|T z|1d?OcG9q$nVHG(lgT6@2IQg8ykUg*0&f=Yy<2$4luO!=^0$_JW8PO;KC`ef|99e~ z9{P*K{@1Cd=J_4ZVJ0|-2Hq^po!8!rxyx5|J!{_J_uJUWy*IqY_biZb9VAa(eaSK4 z&Dv%UfsK;0<`;9>GUpz1H$}Yivk?RGfVHsh2Zt#W-V3~0U~}H5koPS*ul=w#o|*q; zW_RZDmG*lXb|wbof!6{)8yXr3=K^OF_XF-I+>^Mgu{TtgixlC;+yAM_HES_U74kH% zKkk@Ma@oedKNiX1XX4;JsT??N#DQ4sB5Dqj{H8x$>AO&4GVQfIrr(wQ&TRRk&AeR)3T-L&NruAEVaQ|!3vPzC}CjN)ox^bnDh_-)k zI@+^ex#Rwbm1l3e+u| z%MSbfNGR|7WVJQh>fzfdyAR=wteX8h)uLtX`yAVo|NOd>jlEAjvASUQp|-3|+s8S5 zSG^T|(Y^+*x1+62WAo`;tsC!V^Mh%uZNBk#y?XRcJY#=2oU->9`sTZ*Ol=Bpnm%0K zzc&imSo@Wh)j87^xwQ5b+ZJ!!{oV9|H7(W-iy>Z!FEaboS4V#hm;WBcFKQ|psX zeRds@F1gaH5Ob5QI>g@eikms6W%k(9+v0&CCOvR Y`FYPgt(-R{Kr0lWs39?mQJj+WKX(M>%>V!Z literal 5430 zcmc(jze{655XaZ^@JcPyU8>^>(Ldp?@~`k0qL9YIO0SX-4|m<%qKH3VdR-Nh0n%(Gsfie zt=|m|L8HU0YkjXIm@DQM4ZD| zg*f`*`jG?u<`221`2)V6`~@rXdzZgz1DoU(b0a=!_YZg#%}LD-zs9lqw=r%^b<~*R zNUN!xxTorY0ZX09OZzE}dX!a)#Uiy}M*QB9b|U?npF`94Pk4t8x>@-YFR{X`6s%Y1%^d|O*vc7K209vvOo^Ye4p=rhK-jBPuIQaE;F-QOQ_BPRWS_NzcKAd*T2k~!P zV>OIY9Fp}9f*nMIct_hYPFiZzrXKlLh}Kxa2@>qb~!spt|{5U!GW8j*tX0! zD%jS20Vgq_R!KZuZ2>>1D7VHKA8u}LI?S;*y-p*jsctOI{_Zd+15s}Ep;D=IsKcS} zWgTXuPd#_Ft3x@(Kpgk_`nr97e(q`^sFUt2fuH)>uMENs2V6b|dXBkz-Vgtv+;DJS zz%TKyN#ic>&XKhIS-9cg-176+?}*g=S-GS9!~kE2Ns#Z|qX Ny4b`v9B{!2_g{JhND=@5 diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico old mode 100755 new mode 100644 index 4dca9640cb373737df8c7cce41af6dd8160b3913..f9d93b390d8fe6dd63003ea7ebd95c057e7ebc8e GIT binary patch literal 4286 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 literal 5430 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 literal 5430 zcmc(jOG{lz5XalekSvp>ySN%=F^nH!W|dFj3m>T90~AFC-?$J&28p5)+=+vTC~j0# zt{YL@3eL*Mh>9D%hVe{kO<_MfR)6q$?VCa%s{k+1c5@m6VkHm6MaRA@y@mH;^$F2a=hY`MRK> z;A4J%{+EG)0lT`oYIk;aT&0hUF?7)NariAf3yqD9_Tb>a#udiU$<57O@G-zdesOUT zXBx`S&(9qOHu%7t@XO~tK0dZLH#fGctIL*_mb(7o;bDk@E;bq~=BDN3t<`H8nMbGHe3-4d4Ik>uXzBSjZaL;o)Id>1!Mn z02{G)`^t~s(b18WxTM4n8Dm|m&9D9B*-y5^@#+bw_W7;zAs*mUQbsSx6u;re~ zlKQ7q?xUxcwzjs{vR^)k&mW~ON&g?6N6=ql3>|cz%BS`c*UY@UyaB;ZN_A%-jW&r5 zwuVX2+?-uoTXU5@GRAz}z_tUd$MEB~;H)SX_s-1B zjOG4&sGx(c-c3(!Bl*>{QXEw zbf2?k>6GQ=J^QV_wzRYqz9mLRMnZmakiIb%7Z>gB?rx+% zVC%VnlNfMUNj%)$!nuhnpFVwZd{tFd?w#Vr(#i3GI3O=CFOT&Bj=(kvx5VOuV#S^B zL?WR!X}7Jdt&vlKL-_)3?sIo{MH-0k^77Kw)YOD_Y9H51K6LNPI6Uv3bh!i^?$2)( z-jT`&{=~$D-QVALl|C{Zr`_Azi-m(L(A7LbrJ&dOAfHna|(Q z&=5N(xX_jOYYugJBn4CiKBo4g_KCl)sz)1D$XkpFzkWwNivRTVGzAu!&#V0uMG+{! z=zI=h;PPpdF6X?xz5Od}*5rOcrku3h-Q9M4eEhze`%S(^C*bDY61!FyaJO*26~Cyc z$Sy1_xV*@nLLZrqS65ft+uPenZiN2+6PpLmAKyljGeUgjiEHcY>+RLmRV=@M{v-wu z-anI*lWB9SPg7G6>M;Du@a{vA%2c(mKz#|T{ ztZi>^Z|Fl$Pmh!NI#JwF`3L^*$-n-6lWxw|_n5@}tNe=(62}*Nf589J?=O5{9V8Af z^8SRM@{;fGI}gG*xVJFp>qmZns1Lz@i2eQ|hwAqi{pbAtQXgp96T$uPL%%~{nUOt9gA3r@KI2V2Rsy8r+H diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico old mode 100755 new mode 100644 index 8be32dab85a13884518d830f2e3dbf788b75f14f..05962f3f148842ece8c1fe9c996abb1692776672 GIT binary patch literal 4286 zcmb_feQXp(6yK(nYm*XCqks~rw(RX57>zOfTNIPh(llbZQ(EH}A;c(z`jrqdG%EfD z9+ci4rGfMVBcO>9HA+%#jR`1!VTkx0dt7@xx!SvOEiHF5`+RS=({6XS?Oo|K?`C#p z-uu0;c`ucuJp3yuk?2=CwN#SsmLy3*!Yye%67A~)&lY-k&p^JbnN<}uvldq%bGb0U zh86|bcwvx*3j(am738lKwZMG~jZ5-eAy$o;R|=XlP#j{s@^cP5zT?pF1BbeE9MqGH z(HZKKL0yh4ZihP;LJzVT?RA34rb2a?(LV-$V+I!b7*J!=fPT zAncdBIi$HGtZnnh7{EOz7(+cqZbb)IGk=q?_g=^n*rZALx}gL4VLq`aYK4`QTQy&D z%_KIbCHaA3LHUqt94N;zulZleoFHwNC-Nba%ez?L4Y#=hEK!enZ5`x{FaFWs{J_-7 z)fB*Ie{0Y`GPNgGl#>)gSAaKU>x04&JWf2P|FpZlE28Q0(67KIj<q9p94}J*cD_Zy%;r}{hmv$~};L~|#0`BRr4g76g&iGUxs7B3&ulRY>>UORzL)%Dx zVtirnsYX#Q&xBvr1b4ZQ@IT~E2J#68%Nq^+K8JYD$9}4lRI8ovJ%_;SKF)6H*d2(! zhrzvjGP?a2OgSbzF&})Y)zmMn_-;S@MDF04j!m}jN7~7tw860dy^HqzqxZP(G=r+M z9K`9NOBy`#F@u)NVn=8Tdab5@LGM}MmjYIeG9Dtpxu{KY`Zbi)9Cj3{@u=4r1&UgHZ-KHt)W?xaipq@p2llqwZFgq)s zH2ItCZbN#@giZE8mjZReRapG~DBOP3`5n%VY@AZh(rw4yO}5KzoKLfx@G;kg&&BMn zj_UYjHT_WJPs0Okx$8hZi?ERn5;hH6%n#VHW%NDSF6e$=OxJ<@K(UxBA1FVmXAw5_ zZt}Z?|yM)5?km%exO*8Ov=f3FLI!_N%eth6xB(p)zmKvgH-D& z=KKwTD`v$HC2}XmA8U;Kx*ePR@2-j2@P!_flf@s53BTIrBE-Jq7}J&p;9-Gjo#ha} zCco{BEx(HPnS4Lfx^#C&uTjh2VXzGIXT@Fyt0;%@+Z|+4E2jBd*)e)dsf~jpJ)8Pj z^(9*8BBlQNFOK{uSKdC_WX7|u*VLyx>V}BsWSggcxYByx>DyD6^g8jYPee53g|QMR z9`l}B9ZT5wEBj~cPi9`RRyX#gtumDASX`XXHY>G*pW1kz`tSSMc&umY=J-!m8EWld zr}duG_u6NYk6L5k*_!k@-JNOA<4e3W9Y3N`Y-hS}-B&jaq$c?{M$^{wxxTT&=BX9Y zjF91}{x>lfkNMnP8~;l1k}u6?*{+o*uU!%{D5lx+(Y-ak)MJ8otv9(X7hZ7}W3bDR zfxfRBUMKdmot3KS4W*)={+r9omdAY0>xF!^qK~VaVkz@}wrh3E;2CA}V0X6no$ew3 zd3DQByVIF<4?rphOLfvrWKbi{$jFeWAS97`kQ$M2ke{fZ5iLPl e)hJ2jI_>Kz?}6TZg2K=XVMyvU48xFuoA5sgl%aC~ literal 5430 zcmc&&zfV*_5FW%pVgrSR9T#)_NHA3P*l44T{uS01wh#)UcZ3imL~S(MptX`<5Kl=6 z78b@cAsUeoO(-P61Cv96&-wQ4zFRN%9uMx$H_7Jh?as_MZ+2#O-ifrx3F+(k2 z!`_W&!0Za}aU&R=!S^f>^^=(;&0`q`%>@|3Y>TdGsO$n*6n3BI$RKB&ik^`0MNfU&@sI8OXf z-o-C<#Klom{5l=9!{rbC*Ysg4mHJ0V^kxb_&hRRA8o3u-BIjB~DWPyGbs1-0!roDSaGYAp{(-< zb@q)T@}8o9?0XyRfeGNnr#OUUdvmITy7)e{A<7Ttysgj4U#zO#$4{ z8sA^+9iqO!OkdG}?=NQqpSCyt@86%k4aSZpeSc%C0~l_>fymIF$Y5EdYfGeKO{Bdb f(#kQce<8RbhmhmrvtZ@dR2OwpH!+AsOk)29>UVBq diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico old mode 100755 new mode 100644 index f328ff1a5ed098a18d98abf27676e60128e75303..7fa3d4d48d43f7ad2a4d76dd51edda2a863904c0 GIT binary patch literal 4286 zcmb`IeN0|%ZtZ*BjQ(Lv_7_4ijDZA%l5K2$Y|a^V6WpdUMR3Uy zGg~@9Ajr1xF%xAUe{6u?fD(x&&P;I|Er@)ULJKV`jE}z8<2e`J%j@ea6vrcndwcIa z=XdV8=ib9{arl>zz|nv1%Lh2_VUFW?6ddB7MWJ!_;oL@l&}@l!*phZStVy-Kw&b2( zTgtSV9=2=x7| zUJ4Wkb{3cq>pcg`ab!qKzA?q-!el@6Rb_>7)jm*v;>U7jJ#{D_C@0r*FQB9JK8%~s z)p9b-mcOTsa(`7m`Jj#i1_|=(~m{(zcAQ%a8)1ZEY$fx zcaz?SSolwrLE;&!4kU}TQ zFEG2(LO`FtGD-}*XSzPny^ue8qbQf7@i$k%L&i$UX=oC`a9)JM;>*ltFyN6*L~Vz^ zD$wNz`nnVydMD|v*5jMbfW>%DTx9G<-ij8O;JwEOg)V|YOP4z;{ zH`>LEhDIsG*tGXkD2GMZUNFJ<-tLBISY=#~uf8|*R#Uy8`z-SxuY%3S8gX3V61#kv zO?;A;j}Acp*?nPotUy&AEuV5^fu?sQ7O#!E1Y) znV$czR>hWHN6jB>I>hd$vJUSF)hw!;RL6|v;#I>1RlI56^(gPE*tGu_10a}(p|yBF zILgzb-$Mks+D^g9#iQW+<4Q!VsAkdLsdm%e4fT=~wr}E>YS_p(X1EwX@%iiU`zM*; ztlhP$56XU39jaL?akmkhzKAooMzReUd)m7!S-I`Z2Z{qb3r(FP+r?UC|JBGo63I8dGOMslYgro1<1x+D0qAC!|VC9g6+$QP|#w`W2{l{IwQUiFmB z)V78iY0~h4KHKnCFe+R2f!ghFyV)C6c;Ye?efR~uUi39&9lHTS@e&xTMVFST{>>kn z`<&15fUfKf{3+!5>edoI|ISTaeY9&hI9H+OY5QCA117;d5>oF+?wj&w(zNUKew+6A zEwWBnn=%sOU!G6U(@~EJ8E#*g9_4%5Sk(HY)|LChj?pEp4L-x|k>^)y`5f=12;CQ^ zep*dOJ11m&e$(0za=cC2b$Y+I?eagZJpXa3NOc8e8>Wm2-Ad1H>JRo10gs^?)EDBg-~0_IqZYk;h|g`JJx2eCn7;v5Zwmz0P8Rcp-IE64KoP^?0m( zA=lf?XL~zhU9Yze^}B`K=?1+y?HB-;2qA70EO7Uu#DkxUgFz-7baKm5iY+RNDD)us sQ94mdQShTUQ9UEsgz{J^$0af;=}hc|!4g@9U=W7D&twK>0IL-I2d77AhX4Qo literal 5430 zcmc&&L2Oe;5ZwR@Cj>`sWLKScev7><6 za0v>6fI<{{pr|bzN(gq+V8^Ht5Ddl%!6Eja&fD(^KR>(QYa78xt6lHT%$uE^ot%8 zVh(FIB^{gBwWcp^JC{nfm#6RiOvTJidiQcnH;gfdHSmBps0Y7OiTF_F>V8@%JRuul z4Ls>YY$&J$Ix$~)Kj({j9?kvt2hBVZjhh@Xp@K@r#e+v$7sZ{IGX z4(tU48P==M2!0Mzx%7snM~<%00e^rXQ$5?FNnZ4c?a~|MM{&M35;^I^_QMB3|AqC4 z9D0-g!slm`x1AqiU`6}{l+=nJ$Tu6mqGS2}84TQ?!N~gqc_E2O_LtNBEp1XNyH=@@ z)k<}?_)T<$dszoBWFU(?)5h%^ZpcT?67?Nuo#^>$Pr5sPE|rK4b6!YsU-X+E4`U5H z;B6L9Rf-x601}|hFD|+hfC$8^p9Z$wi@IL1Ri}ljfzz_7|uhaB; ztXJn!+%b$Xhc)mBKV+c;y6VA)-RVU9uIQP#yo2%&FVf=M=Ybnw9)btFkP&(4a`i@=eoNVxO)CVoVs`|)(zZ<|8Z9!k###Ve? zKd=w#^uJ)Ew_(b>ga3!Y>k@xKTp0_Ft*;e-x}IWu)DDf14OixC-9O|2@Y?fFv{iy* z>Rs|rt^6B}Za;t5{EIiB*83A>gK6LOyg$Hqd4Fv-gz+1Bf3*)#>sI3cwSp^YnI`KG z;5Sl#!N!kOf58TvgVer${ps!po4;J^PvC<-XZ`J4u;hm(zCXH_>MTE|+`m7>2E>Eu z`wO)La<%FE3u{u_=zG!N_m|j!9x;LZzJ3AxKj-Nchl?ROPd*3p$}27{CH1u>S`nzphCD diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico old mode 100755 new mode 100644 index b4394e1b4af94ab7efe98ed2c5dad06c49434c7f..b0c26b62068063c7d9a343be7d5d2feef25a4c42 GIT binary patch literal 4286 zcmb_eZ%kWN6n_L0KhDJHC!-{8ukUp;`oZwcpw5I18sp?o+%USi7_-bROg9r_;)sbR zCfjh3vEbse0VZaUn3$-El86opT?~q4!fRgNdxh3<$*=d_ zKj-|;pL@@RPbHH{X2W(I3eLsAz`#?;opih1`8jQ34#k0TfqI$? z%>&A{;+c~-HZ}~J?1CMk!A3;D+2m%hPQNhTBu8FWgL1S2CO85>fm6buaw?PvqtO|-c? zAO7dALD}ifzy|S??Pje|8>l`Y|Lku3LZjSt;I6D(=<>0c5l^m0YnW5!cQ_mdiUXcm zHrAdGlBXJl{v>np?{9)9EY0%2e7gkvZ3)iYm}mT(o13}l!SDC8J;TGpkWQyzW@d(s zM@L5u^=bPIuA1>%zkwRdchV-~ZbSZ0B{=?dkm;iPW7Z1cCV8sW82?c{zok*?>mJt=iFI<6h2X0GJ7WXaWtk>|LO~JeM z6HJco`~PiJo9N9#-`%LA*m7O^iSN+FoBY0m_(a1^{=X9g@vslRzxM#__Wqpra5uZ? z%_86F?Iz#(JF=YRH}N;u1H~bqT+n+#Z`L+<8^wK#9dq9(wPyTpLGJTLe#@zNhFi6P;y}3|opdJY zUeMj7`apGv>Lk@_I)}aZHvDzJPr{gARC(22XoM2J9rKShhX2^eP3PY7dceT1+CgWs zqsztYAY1hK{r&)a^{Yo|_;Kb1l~dpIH{~GznK?KAVLB(bY6JD97Za>TooSkf`W7FY zx)uQ27hw=TNBdi(1wE&BEPRmoR;UVrDc+8ah_&HcyFsX1`^Qus_44m0nzVfS?}K$Q zt9W=$(fXr)TkE1n_501n))SE$RVJ%{ZB7wRZj_kGXy-&*Ae<#f=l$q>&i6?xtA2|| z{3*Q-!4?Rq>pS&A)pD0X_RW<)caqVsiN`_?y$-Qzxm~}{Y<%dg$cy?MSdT|)&GzP+ zv(|>+%jTa`D7U%x>*vMF#aKrE$U;iLzS|hEX7j0 zmhZRlM(}S{2jw(hIofxyHuj1td#EOIx)51)FXrIliD;=hpKmWzEUXLV^Yl+% zQNA+TeKj>uESCFau`&?T&gc6rRxjTZjxGO@?|!p=C~p_5SMQpw>E{5rGElgEkmMdk z6hnk70xtsxJ=_)=utz}&K?Na#=s`3fuu+`oJtOQxJk!8&Wek^l%6h4+XxT< literal 5430 zcmc(j&r4fD5XZ+_>QRrqnTw@|(tn{n3H~SjFDm{4HEN7E5k)Qj0}4hF4_*{JDB@3$ zAc6-aNRSAU?R?gq@C@(O_e|QiFzjY`<~y^qv$OLe(j)I=U_i7l$dC6TpG2fkc>euc z=YQ$kXPwibay-v#m#U47jC?7V%f<2W@k6EiK!?n+mIN|1H1wfZES4rFCZ1MTSKZ0U ziMzSE@x+)p*5H9Rio^E4C4!>O|}16`+F#iH^bU6 zyq)SZ`RVEDkLvrfV%JoUzca7(@FIh(;biS6Cnw2$V@h??r?_r;dD(4mZ@c~deRq0# z>WMLPtifaaL3RwREc&rqadr%6W@g6i?d`dGy>96wv4;meSR!;yZ=9kjbKtHrPt0bC zHR^zBwVGN6Y`Yie#0Md-`P{<2$$ ziQrEkUukSWhsA;RKA(fJw6qk3PBp%`xS-z3Yy-N&p6xC1xgwR)HYRd#RvRo1b^rby z!xnOQQae6Ab{iWTu2QMQ(%jr!%Lia_*Kcocli<8A_>}w`%9wq`wd?Ea7uZ4W)w%Wc z^*A^`hpvEQ?{_Zx)&IVBz?K~SPYmQ1f9iu0`l%hVwl3YjVf3pVx#XX`^Dl2ep6?I( z^L~G^L1Um*e^4uQyZx>HK!4u#7dCJXdJkIlXE*!+Mx^$uM(ZAQwl~$ETm5ae1V6m_ z`-2+C>a37kQh$G#4a7r;$rs;W)DFDgao=C8k;l*(2-(cvUuFY6z92s+?tlIMjM~81 zYDKlfYNoDyf4f@0bE7@3Df074N;5wCtssA?SYU#U K4s@Xt-G2b{MpTsm diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico old mode 100755 new mode 100644 index 4f436c9524219a91bde74d32eda45fd37c6acef1..b150960b5befbb2a8ae5a61cdcb64437ac9a98d8 GIT binary patch literal 4286 zcmb_dYfKbZ6dpp8{o*G>!YC zHnFuuAVMht1x$;(KnMi+Q5$Gu6dK#w*xIC|%5wn$cX4?$JE!-|I-A*o~9^@Aw*bI5+USEgY2#J;2c~uUvEs~9ENh$ zTyvjy8fP<{#$L{8xI*|xILEUwD~*f#GMokr?=alv9IL_B@QiTk7aNq{I0n@>?NHQt z5c1l#3Ao2Q4GUy0+1F@zawY6QpNU^zqwd_+1mS956U_T&L2nFs@4&Y=@`(*bkx+j| zqhaH6Z6Me+h+Xkp0SMxP9;}AfJ^lwAXx=1{cug+H27>zuVqf~}D-A|C#`v_$Zv%D&7!E7cKE?raL3}7#Y98>8XF6kiBa?l)>V2XB(+ghRJtfnBAMj2_ z{x0n8NssZ)HSVI$T&1eOdTRaw9RJAz*-i0q$9*fn*IVG7#2oVVhL0nS!7j(vLl=g zSRb%Pg~MkGeZ}jled5;h#gblie!B+RBtBwa>u&~(ZBEOJVQs(~g?Bj={@P-A%2eX} zm#!6nt`Z>Obfu)zJu?#IKYp}zS3%dnfYfWXsPyiRPi~QJ#k7Oy`0eP3Vfk=MK8Q3rU@6VWq z_{I-1u*rR}>s&It+sJ6%VWb9O&qCj^ccbt01)nd-Z^XB&unE4&;*#(;?Cu2XMKc_1 zNm#NC*s~BDdpBa^(B%A#7MmuqN8e@IoYgAXfN^NQTNm~|V86hg^$?r%ZZ6u7PI6x; zln4ATCD@0x_@?a10Jdxc#sPDII`K~8yTG@J^#N-X)=8|@*e^1ivE;4A3cPc5zKko+ zOd-V4wIu(f#?VK#*m&Bf9uKpVkMq>9dGlR+2npv>5sfN+a`9Z&+C1gvpk$E6EyGW z7n%Jtae6XJ9%~tP2g%`m-#HkvKPhG9HJdsxuhzj>hDYW7#b(AjR`Yx*-$()?|v^BzhA`MhB~k2v*|o8kJ+~H)D`zGsXt1OGLRPIH(W_I7ZDeJ(TMkW zp4U4^po8+0oy3y8MaW&tAE;ZHQcy0sq?I(i&+PYYv+MV}``uk*9}F`)^WOWsdGqGY`>qf< zVx1@`5Y!imW9x<3DugI1iu`V-`Ffh$MsqYIj>tT9wy30H^Y)sqvIb|1BS3AzXaUS& z%}U9Z9UJqiT1y+NJiAt!2G7ZXnQr+p^g%U@F^4tqfLGUp-<2(n9+&s9oLiWdR)jV1 zfLGT6otR(x9kvpU&VvWMkbw=@6YNTa8x=MN;DrqP3VVyEj`3Tvp}$7{_+@TW(_pP+ zKf^ydwT<17!z|4Ql$Nl8DcRW-*`!Cb;LkwgRKakHfepv_Xu)aUF zmiGtpOdOLO8^#%1-rwayA%Yu(@Z}2O%84}A72oqZcp(E>J0c$iuF0Osdvf4&w`v$;4r|2!&qm0S4(Lh+A9m9o`Wg3Bd%5f! zzb)sNzQ)`&vmf9AFJxFAy6pO*y0v&Go#`hfOM7|fucth>Xa<#V4ldQ4}2ED(&G_6p*0ofIJ5snDjp(x4D07WXXD2CAZF8vHM{mgw{|bw@PU!n zRBpk&u|uzdPgM;uJwIZHJ%$JMhl)kYXPi^<8)swZes@mXiQemzZF}U@cYlojWIuqx zaX|4N$>)TBfpS+e(y^7zB$tj2z%qQG-V%(FKNFD|cDgnIOUWAV7{@VXFB#rv4HLW@>;Pz@L!G2Lx)`(fCYZ1r&zt; z$?y?l(5+*$t?Agn__TlEj{-lD|A95PI7K5hF? ztO_gQf2yOXX2KY?L#CwLfS9!+{r({b7`E8TKUO+MZvgE-Ywr*6v;F=mAE4G%IRW)YZwel@$C{}A0DiXXFW7){K#d4LMC#AX{b0BL z1U@L6^>@Sq!w(sJf8dU3b(Tb3t=D^ulc+~Ln0TYJ? z`%||8W3Cm|d$ES!-!hahr7K7N5#s!c5PO$}C=3aa9~5GPPl#MJ7yX66O(Y@aL+>-> QgQN>Op&J;$0w%Ek0@kn7od5s; diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico old mode 100755 new mode 100644 index 805cc20cdec49d82bc29a06676e8263a89425a9d..7e71d71684df725e091c00c67a6d763c898bdedd GIT binary patch literal 4286 zcmb_fYitx%6uzZRz-UqvsYIi;fp&Ig!Qc<$->PT|r5dphh5AB5{Grq*8YI}&#Kc5! zA4Qs4X-bRHpfSb-jY>lO0c!lCL5q(+txK0mOS@%x^f5D^=iJ%ZOlN0yr_$4$o|$v+ zIo~3$kizvEEVmP%VjtP>} zB!@)|$4LJYt>3ojxa=^72ErO73@lB+xc_rt+jl_Q0MK$2sP2W%YqV#M_I8N`M(*5OH~CxETr${0+lNm3-R_NrruqK~n+9|L9#PuM)=$rMo01)XYf z@v35oVJBnnI*|t<6(9WpwEqCGbkooUD+XDI|8o`z+`@OO{tK9<+j;iAG!AflEWL>9 zKt4=)9~OELbjQiB@pszGv!x^75}&0XW$(k>g1(F6C|eT4#fZ zdsGp8HEJYk4jJb14nx*#V!DvmY+bpw#D$1^6yIe(`04~ILLJ6mO1}=JM@PjQ7Jf2Q z$ZK$X?vs@Ngcp7|hUU;geMVyU5&kY6H@&9IoKDd9Q;6if3XnhdYRdnV8$T4)KUMk+ zQ(|-OZxip(vGN7RHxc=<5PGouxmWYNpjf+X{8jIxI@GD3&EYb3+`?vj=4Efrpt{F| z?$>-Mv>^HOypa2NZW5_`v7@F(*XLn(6DH+sV=K?j;^xQ4rCs-L{xv9u&+~$37S(=G z&>RcJ^)Hpfj(T(6y~M9NusQ!HGEmq3gKHwwxNg7c`yFb1R&Ff4h-a3Jk!CmNuJjtl zyu2Ad;=(5UP(unD;jemN1ZD3Uxb;K-7`$-#$arRD>%Nb%nextjZ$38Hp0RDNY#Nm@ za2@z83f%{eh35s&tnAtJaosI+I{T>JmCOGCVYlVuhqhkKVcRiq9r!HRC*Mh)x%_Q% zf8ZX)eUf`M&x_Ky37Q=`&6`(jTwAJ9R4M&*{?ucXm-Dgt{)HYK&BNz7_&)HRT=v#1 z#bH{t&3^mYTv0~Y<(;N^Z4`Idm~N?b8t0VV-q^}>bDr3C9c;Ps`8nC6Ds~%KzQ@3d z*XfSz)KRmC;y<9L+?dXH^_fc_sEuRr0t_kOSU@RoG5(=PXXU(!^cx>Dt3t;o@tsdjTl-mC ze`eP0Lv0uxv)2nrwQ>BjJlluQe_zPPy{2s%`^oJ?YaH))@AG@H&+0?xezAS^;n$z1BVr%cEGibAXQ0=!&-DT^8O0_<9S*;u4 z|K>`9+Bo+$H=|UmJ7Z`YM>9@+&}Xe_{D`_~{CLp)Ui--Yq}DWX$ZO55qls8clc}8k z5?xAMinJ)fkYs@YVR3{c8S2E`5Yog0#1Ud5t`pBQ=5pf7h!C}s+Xrd~Fw}1IForOU MwB#vFA%hwG2hr)3h5!Hn literal 5430 zcmc(jziU)M5XU!060k`M3%eY0`2mqs_Oa1M8~rQ9(tjW!g~(k9A%=i9N^Hc&Mg=9r zvk6$(oe~7~v@sTHI3pwy9Otv|&AB|^-Fq)*yn$i&c4y|hGdnxG@0{y%N8IqR)3)R; zA9ZfbIaex0zwhbzf{sn-n08en8rLQl9X~sBYJ5o+%N40BH)F0d##}oi$4*WUl;>r6 zVqW&IJ`{PpDe`J7V)~3RhYYfz9e$5iWbMLjk+)x@EirdqWk##A7TUljx;uM+RQL8p zDz%8|8zF-YbXWsEv92U;@GVK;TzL?2xGs}J$Kw1*Wxj;^Y`;<3- z(6{`>Mlz4WxU|b(v%xy-{16{v4AaNocIO9uBg;w4jd+jSp5NUO=Nf~~)%u;A?TgrK zC0m)55EYT)HcAUF+AKmr*U{jL~O|IgLZ>e4~pEY!$)x zK38PJa8KV5x%EP1>wB2B>3w97MaT5DhHvDJ#duYwRQ{vaRbQ?>6KU?IV|5xu2VIK? zHYXOO?L9?&V!u81)S}4KH=Pjj%{@^Y*uo#y&@K*szvGxAU)S^9iGKVP`S2}b`ke?{ z!5TKw^1)J z6f4{Z{k?(>ayCz{9TXqib}qp5Ij6Ns`@XhtZt~FU&wu;RSRT5m82AdN)l?rx-S@0L z4DAzxRjoU4p4gi%7?uaEdamoaxM~uI--3nDt-D0~)XyUHUf+KB4L9^079p_WnEQK- zJ~ccqf$jBG&)Onu04v7#cPRa|w_3aP;IrXcTO-`)<2UnKtB`X9z$ z&%!#fD3X8O;)nU?&w%3dcF0TJ;)myt8sKxNK>g_kKUsgMfko?YH~7i=%N@X7Kn!|u z|M)v7dgJgD@8 zT0!d_e&Fv9&X~Qk$SJ*Na({o=8hk#ae1B29sGrnu?y>Ou%hxrnV?K|;J>d73t${Xv z8~^w3&u|TX9f$P&E!#sPvwiZ*xtV?Ergxni*>-NI;oM-&xqf(7|3WZ!8Dw&_wOGHQ Nwy=q9Fu(#6?B6 Date: Tue, 25 Apr 2017 18:36:05 +0100 Subject: [PATCH 02/22] Added test --- spec/serializers/status_entity_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index c94902dbab8..3964b998084 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,6 +18,12 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path + expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico') + end + + it 'contains a dev namespaced favicon if dev env' do + allow(Rails.env).to receive(:development?) { true } + expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico') end end end From ccac05dd90cb5cfa9abbeb11a50f953541eb83bb Mon Sep 17 00:00:00 2001 From: mhasbini Date: Thu, 27 Apr 2017 01:04:07 +0300 Subject: [PATCH 03/22] Fix 404 when upstream has disabled merge requests --- app/helpers/merge_requests_helper.rb | 6 ++- app/models/merge_request.rb | 7 +++ app/models/project.rb | 8 ++++ app/services/merge_requests/build_service.rb | 2 +- .../merge_requests/_new_compare.html.haml | 2 +- .../unreleased/26488-target-disabled-mr.yml | 4 ++ lib/api/merge_requests.rb | 2 + lib/api/v3/merge_requests.rb | 2 + spec/helpers/merge_requests_helper_spec.rb | 46 ++++++++++++++++++- spec/requests/api/merge_requests_spec.rb | 13 ++++++ spec/requests/api/v3/merge_requests_spec.rb | 13 ++++++ .../merge_requests/build_service_spec.rb | 10 ++++ 12 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26488-target-disabled-mr.yml diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index e347f61fb8d..2614cdfe90e 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,6 +1,6 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project + target_project = event.project.default_merge_request_target new_namespace_project_merge_request_path( event.project.namespace, event.project, @@ -127,6 +127,10 @@ module MergeRequestsHelper end end + def target_projects(project) + [project, project.default_merge_request_target].uniq + end + def merge_request_button_visibility(merge_request, closed) return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9d2288c311e..365fa4f1e70 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base 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? + validate :validate_target_project, on: :create scope :by_source_or_target_branch, ->(branch_name) do where("source_branch = :branch OR target_branch = :branch", branch: branch_name) @@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base end end + def validate_target_project + return true if target_project.merge_requests_enabled? + + errors.add :base, 'Target project has disabled merge requests' + end + def validate_fork return true unless target_project && source_project return true if target_project == source_project diff --git a/app/models/project.rb b/app/models/project.rb index c7dc562c238..9d64e5d406d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1314,6 +1314,14 @@ class Project < ActiveRecord::Base namespace_id_changed? end + def default_merge_request_target + if forked_from_project&.merge_requests_enabled? + forked_from_project + else + self + end + end + alias_method :name_with_namespace, :full_name alias_method :human_name, :full_name alias_method :path_with_namespace, :full_path diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index d45da5180e1..bc0e7ad4e39 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -28,7 +28,7 @@ module MergeRequests def find_target_project return target_project if target_project.present? && can?(current_user, :read_project, target_project) - project.forked_from_project || project + project.default_merge_request_target end def find_target_branch diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 8d134aaac67..9cf24e10842 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -38,7 +38,7 @@ .panel-heading Target branch .panel-body.clearfix - - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] + - projects = target_projects(@project) .merge-request-select.dropdown = f.hidden_field :target_project_id = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } diff --git a/changelogs/unreleased/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml new file mode 100644 index 00000000000..02058481ccf --- /dev/null +++ b/changelogs/unreleased/26488-target-disabled-mr.yml @@ -0,0 +1,4 @@ +--- +title: Disallow merge requests from fork when source project have disabled merge requests +merge_request: +author: mhasbini diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e5793fbc5cb..710deba5ae3 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -20,6 +20,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 3077240e650..1616142a619 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -23,6 +23,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index e9037749ef2..10681af5f7e 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -64,7 +64,7 @@ describe MergeRequestsHelper do it do @project = project - + is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3") end end @@ -149,6 +149,50 @@ describe MergeRequestsHelper do end end + describe '#target_projects' do + let(:project) { create(:empty_project) } + let(:fork_project) { create(:empty_project, forked_from_project: project) } + + context 'when target project has enabled merge requests' do + it 'returns the forked_from project' do + expect(target_projects(fork_project)).to contain_exactly(project, fork_project) + end + end + + context 'when target project has disabled merge requests' do + it 'returns the forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(target_projects(fork_project)).to contain_exactly(fork_project) + end + end + end + + describe '#new_mr_path_from_push_event' do + subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h } + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user) } + let(:fork_project) { create(:project, forked_from_project: project, creator: user) } + let(:event) do + push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user) + create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data) + end + + context 'when target project has enabled merge requests' do + it 'returns link to create merge request on source project' do + expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id) + end + end + + context 'when target project has disabled merge requests' do + it 'returns link to create merge request on forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id) + end + end + end + describe '#mr_issues_mentioned_but_not_closing' do let(:user_1) { create(:user) } let(:user_2) { create(:user) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4bff1647b5..16e5efb2f5b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -434,6 +434,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it 'returns 422 when target project has disabled merge requests' do + project.project_feature.update(merge_requests_access_level: 0) + + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + end + it "returns 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 6c2950a6e6f..f6ff96be566 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -338,6 +338,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it "returns 422 when target project has disabled merge requests" do + project.project_feature.update(merge_requests_access_level: 0) + + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: "master", + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + 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 diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index be9f9ea2dec..6f9d1208b1d 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do end end + context 'upstream project has disabled merge requests' do + let(:upstream_project) { create(:empty_project, :merge_requests_disabled) } + let(:project) { create(:empty_project, forked_from_project: upstream_project) } + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(project) + end + end + context 'target_project is set and accessible by current_user' do let(:target_project) { create(:project, :public, :repository)} let(:commits) { Commit.decorate([commit_1], project) } From 6236705ef6413fdc58a1d5514fc34d1f481d654f Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 27 Apr 2017 02:03:29 +0000 Subject: [PATCH 04/22] Gitlab -> GitLab --- doc/administration/integration/terminal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3b5ee86b68b..91e844c7b42 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -32,7 +32,7 @@ In brief: As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers -through to the next one in the chain. If you installed Gitlab using Omnibus, or +through to the next one in the chain. If you installed GitLab using Omnibus, or from source, starting with GitLab 8.15, this should be done by the default configuration, so there's no need for you to do anything. @@ -58,7 +58,7 @@ document for more details. If you'd like to disable web terminal support in GitLab, just stop passing the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse proxy in the chain. For most users, this will be the NGINX server bundled with -Omnibus Gitlab, in which case, you need to: +Omnibus GitLab, in which case, you need to: * Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file * Ensure the whole block is uncommented, and then comment out or remove the From 5efd294d77e9eb7366fe71fdfd49314376cb7683 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 27 Apr 2017 02:06:08 +0000 Subject: [PATCH 05/22] use TCP or HTTPS protocol for port 443 on load balancer --- doc/administration/high_availability/load_balancer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 3245988fc14..2f8037c454b 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -13,7 +13,7 @@ you need to use with GitLab. | LB Port | Backend Port | Protocol | | ------- | ------------ | --------------- | | 80 | 80 | HTTP [^1] | -| 443 | 443 | HTTPS [^1] [^2] | +| 443 | 443 | TCP or HTTPS [^1] [^2] | | 22 | 22 | TCP | ## GitLab Pages Ports From e985d5a56f14dc78ba0b6f0d369035a41b080c7b Mon Sep 17 00:00:00 2001 From: menway Date: Tue, 28 Mar 2017 15:21:57 +0000 Subject: [PATCH 06/22] Change to correct directory in update instructions [ci skip] --- doc/update/8.10-to-8.11.md | 2 ++ doc/update/8.11-to-8.12.md | 2 ++ doc/update/8.12-to-8.13.md | 2 ++ doc/update/8.13-to-8.14.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index e5e3cd395df..e538983e603 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. 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 ``` diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index d6b3b0ffa5a..604166beb56 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. 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 ``` diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index ed0e668d854..d83965131f5 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. 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 ``` diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index aa1c659717e..aaadcec8ac0 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. 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 ``` From 6f89bd3628322d0c17f97163d060e6ef7238de3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Wed, 19 Apr 2017 11:06:20 +0200 Subject: [PATCH 07/22] Use Gitaly for getting Branch/Tag counts - Backup-rake-spec fixed. Storage config broken - Use rugged to compare branch/tag-count in specs - upgrade gitaly --- GITALY_SERVER_VERSION | 2 +- app/models/repository.rb | 8 ++------ lib/gitlab/git/repository.rb | 29 +++++++++++++++++++++------ lib/gitlab/gitaly_client/ref.rb | 8 ++++++++ spec/models/repository_spec.rb | 5 +++++ spec/tasks/gitlab/backup_rake_spec.rb | 6 ++++-- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a918a2aa18d..a3df0a6959e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.6.0 +0.8.0 diff --git a/app/models/repository.rb b/app/models/repository.rb index d02aea49689..ce1238e66d2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -505,14 +505,10 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - def branch_count - branches.size - end + delegate :branch_count, to: :raw_repository cache_method :branch_count, fallback: 0 - def tag_count - raw_repository.rugged.tags.count - end + delegate :tag_count, to: :raw_repository cache_method :tag_count, fallback: 0 def avatar diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 452dba7971d..4af7f1396c7 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -122,13 +122,30 @@ module Gitlab # Returns the number of valid branches def branch_count - rugged.branches.count do |ref| - begin - ref.name && ref.target # ensures the branch is valid + Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_branch_names + else + rugged.branches.count do |ref| + begin + ref.name && ref.target # ensures the branch is valid - true - rescue Rugged::ReferenceError - false + true + rescue Rugged::ReferenceError + false + end + end + end + end + end + + # Returns the number of valid tags + def tag_count + Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_tag_names + else + rugged.tags.count end end end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index d3c0743db4e..2a5e8f73e55 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -34,6 +34,14 @@ module Gitlab stub.find_ref_name(request).name end + def count_tag_names + tag_names.count + end + + def count_branch_names + branch_names.count + end + private def consume_refs_response(response, prefix:) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 98d0641443e..4526c3194b6 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1379,12 +1379,17 @@ describe Repository, models: true do describe '#branch_count' do it 'returns the number of branches' do expect(repository.branch_count).to be_an(Integer) + rugged_count = repository.raw_repository.rugged.branches.count + expect(repository.branch_count).to eq(rugged_count) end end describe '#tag_count' do it 'returns the number of tags' do expect(repository.tag_count).to be_an(Integer) + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync + rugged_count = repository.raw_repository.rugged.tags.count + expect(repository.tag_count).to eq(rugged_count) end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 0a4a6ed8145..df2f2ce95e6 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do before do FileUtils.mkdir('tmp/tests/default_storage') FileUtils.mkdir('tmp/tests/custom_storage') + gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address storages = { - 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') }, - 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') } + 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }, + 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + Gitlab::GitalyClient.configure_channels # Create the projects now, after mocking the settings but before doing the backup project_a From a998710a3b2d4d2f5fa7dbdaf4ae468cd304730c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Wed, 26 Apr 2017 18:33:48 +0200 Subject: [PATCH 08/22] Fix RSpec --- spec/lib/gitlab/git/repository_spec.rb | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f88653cb1fe..1b78910fa3c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1074,20 +1074,8 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#branch_count' do - before(:each) do - valid_ref = double(:ref) - invalid_ref = double(:ref) - - allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) - - allow(invalid_ref).to receive_messages(name: 'bad-branch') - allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } - - allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) - end - it 'returns the number of branches' do - expect(repository.branch_count).to eq(1) + expect(repository.branch_count).to eq(9) end end From c504b88f07f03d381e7b2e22a129413319508413 Mon Sep 17 00:00:00 2001 From: Alexander Randa Date: Thu, 20 Apr 2017 08:31:37 +0000 Subject: [PATCH 09/22] Implement ability to update hooks --- app/controllers/admin/hooks_controller.rb | 26 ++- app/controllers/projects/hooks_controller.rb | 13 ++ app/views/admin/hooks/_form.html.haml | 40 ++++ app/views/admin/hooks/edit.html.haml | 14 ++ app/views/admin/hooks/index.html.haml | 55 +----- app/views/projects/hooks/_index.html.haml | 24 ++- app/views/projects/hooks/edit.html.haml | 14 ++ app/views/projects/settings/_head.html.haml | 2 +- .../integrations/_project_hook.html.haml | 1 + app/views/shared/web_hooks/_form.html.haml | 182 ++++++++---------- changelogs/unreleased/19364-webhook-edit.yml | 4 + config/routes/admin.rb | 6 +- config/routes/project.rb | 2 +- spec/factories/project_hooks.rb | 2 + spec/features/admin/admin_hooks_spec.rb | 43 ++++- .../settings/integration_settings_spec.rb | 94 +++++++++ spec/routing/admin_routing_spec.rb | 14 +- spec/routing/project_routing_spec.rb | 6 +- 18 files changed, 372 insertions(+), 170 deletions(-) create mode 100644 app/views/admin/hooks/_form.html.haml create mode 100644 app/views/admin/hooks/edit.html.haml create mode 100644 app/views/projects/hooks/edit.html.haml create mode 100644 changelogs/unreleased/19364-webhook-edit.yml create mode 100644 spec/features/projects/settings/integration_settings_spec.rb diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index cbfc4581411..a119934febc 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -1,4 +1,6 @@ class Admin::HooksController < Admin::ApplicationController + before_action :hook, only: :edit + def index @hooks = SystemHook.all @hook = SystemHook.new @@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController end end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'System hook was successfully updated.' + redirect_to admin_hooks_path + else + render 'edit' + end + end + def destroy - @hook = SystemHook.find(params[:id]) - @hook.destroy + hook.destroy redirect_to admin_hooks_path end def test - @hook = SystemHook.find(params[:hook_id]) data = { event_name: "project_create", name: "Ruby", @@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data, 'system_hooks') + hook.execute(data, 'system_hooks') redirect_back_or_default end + private + + def hook + @hook ||= SystemHook.find(params[:id]) + end + def hook_params params.require(:hook).permit( :enable_ssl_verification, diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 1e41f980f31..86d13a0d222 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,6 +1,7 @@ class Projects::HooksController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! + before_action :hook, only: :edit respond_to :html @@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'Hook was successfully updated.' + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) + else + render 'edit' + end + end + def test if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml new file mode 100644 index 00000000000..6217d5fb135 --- /dev/null +++ b/app/views/admin/hooks/_form.html.haml @@ -0,0 +1,40 @@ += form_errors(hook) + +.form-group + = form.label :url, 'URL', class: 'control-label' + .col-sm-10 + = form.text_field :url, class: 'form-control' +.form-group + = form.label :token, 'Secret Token', class: 'control-label' + .col-sm-10 + = form.text_field :token, class: 'form-control' + %p.help-block + Use this token to validate received payloads +.form-group + = form.label :url, 'Trigger', class: 'control-label' + .col-sm-10.prepend-top-10 + %div + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. + + .prepend-top-default + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' + .col-sm-10 + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml new file mode 100644 index 00000000000..0777f5e2629 --- /dev/null +++ b/app/views/admin/hooks/edit.html.haml @@ -0,0 +1,14 @@ +- page_title 'Edit System Hook' +%h3.page-title + Edit System Hook + +%p.light + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be + used for binding events when GitLab creates a User or Project. + +%hr + += form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f| + = render partial: 'form', locals: { form: f, hook: @hook } + .form-actions + = f.submit 'Save changes', class: 'btn btn-create' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index d9c7948763a..71117758921 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,57 +1,17 @@ -- page_title "System Hooks" +- page_title 'System Hooks' %h3.page-title System hooks %p.light - #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be used for binding events when GitLab creates a User or Project. %hr - = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - = form_errors(@hook) - - .form-group - = f.label :url, 'URL', class: 'control-label' - .col-sm-10 - = f.text_field :url, class: 'form-control' - .form-group - = f.label :token, 'Secret Token', class: 'control-label' - .col-sm-10 - = f.text_field :token, class: 'form-control' - %p.help-block - Use this token to validate received payloads - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10.prepend-top-10 - %div - System hook will be triggered on set of events like creating project - or adding ssh key. But you can also enable extra triggers like Push events. - - .prepend-top-default - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' - .col-sm-10 - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification + = render partial: 'form', locals: { form: f, hook: @hook } .form-actions - = f.submit "Add system hook", class: "btn btn-create" + = f.submit 'Add system hook', class: 'btn btn-create' %hr - if @hooks.any? @@ -62,11 +22,12 @@ - @hooks.each do |hook| %li .controls - = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' .monospace= hook.url %div - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 8faad351463..676b7c345bc 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -1 +1,23 @@ -= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{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([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Add webhook', class: 'btn btn-create' + + %hr + %h5.prepend-top-default + Webhooks (#{@hooks.count}) + - if @hooks.any? + %ul.well-list + - @hooks.each do |hook| + = render 'project_hook', hook: hook + - else + %p.settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml new file mode 100644 index 00000000000..7998713be1f --- /dev/null +++ b/app/views/projects/hooks/edit.html.haml @@ -0,0 +1,14 @@ += render 'projects/settings/head' + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{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 [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Save changes', class: 'btn btn-create' + diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index e50a543ffa8..5a5ade03624 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -14,7 +14,7 @@ %span Members - if can_edit - = nav_link(controller: [:integrations, :services]) do + = nav_link(controller: [:integrations, :services, :hooks]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span Integrations diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index ceabe2eab3d..8dc276a3bec 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -9,6 +9,7 @@ .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + = link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do %span.sr-only Remove diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index ee3be3c789a..37c3e61912c 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,102 +1,82 @@ -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - = page_title - %p - #{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| - = form_errors(hook) += form_errors(hook) - .form-group - = f.label :url, "URL", class: 'label-light' - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-group - = f.label :token, "Secret Token", class: 'label-light' - = f.text_field :token, class: "form-control", placeholder: '' - %p.help-block - Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. - .form-group - = f.label :url, "Trigger", class: 'label-light' - %ul.list-unstyled - %li - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered by a push to the repository - %li - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository - %li - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This URL will be triggered when someone adds a comment - %li - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This URL will be triggered when an issue is created/updated/merged - %li - = f.check_box :confidential_issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :confidential_issues_events, class: 'list-label' do - %strong Confidential Issues events - %p.light - This URL will be triggered when a confidential issue is created/updated/merged - %li - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This URL will be triggered when a merge request is created/updated/merged - %li - = f.check_box :build_events, class: 'pull-left' - .prepend-left-20 - = f.label :build_events, class: 'list-label' do - %strong Jobs events - %p.light - This URL will be triggered when the job status changes - %li - = f.check_box :pipeline_events, class: 'pull-left' - .prepend-left-20 - = f.label :pipeline_events, class: 'list-label' do - %strong Pipeline events - %p.light - This URL will be triggered when the pipeline status changes - %li - = f.check_box :wiki_page_events, class: 'pull-left' - .prepend-left-20 - = f.label :wiki_page_events, class: 'list-label' do - %strong Wiki Page events - %p.light - This URL will be triggered when a wiki page is created/updated - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification - = f.submit "Add webhook", class: "btn btn-create" - %hr - %h5.prepend-top-default - Webhooks (#{hooks.count}) - - if hooks.any? - %ul.well-list - - hooks.each do |hook| - = render "project_hook", hook: hook - - else - %p.settings-message.text-center.append-bottom-0 - No webhooks found, add one in the form above. +.form-group + = form.label :url, 'URL', class: 'label-light' + = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json' +.form-group + = form.label :token, 'Secret Token', class: 'label-light' + = form.text_field :token, class: 'form-control', placeholder: '' + %p.help-block + Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. +.form-group + = form.label :url, 'Trigger', class: 'label-light' + %ul.list-unstyled + %li + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This URL will be triggered by a push to the repository + %li + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This URL will be triggered when a new tag is pushed to the repository + %li + = form.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = form.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This URL will be triggered when someone adds a comment + %li + = form.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This URL will be triggered when an issue is created/updated/merged + %li + = form.check_box :confidential_issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :confidential_issues_events, class: 'list-label' do + %strong Confidential Issues events + %p.light + This URL will be triggered when a confidential issue is created/updated/merged + %li + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This URL will be triggered when a merge request is created/updated/merged + %li + = form.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = form.label :build_events, class: 'list-label' do + %strong Jobs events + %p.light + This URL will be triggered when the job status changes + %li + = form.check_box :pipeline_events, class: 'pull-left' + .prepend-left-20 + = form.label :pipeline_events, class: 'list-label' do + %strong Pipeline events + %p.light + This URL will be triggered when the pipeline status changes + %li + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This URL will be triggered when a wiki page is created/updated +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/changelogs/unreleased/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml new file mode 100644 index 00000000000..60e154b8b83 --- /dev/null +++ b/changelogs/unreleased/19364-webhook-edit.yml @@ -0,0 +1,4 @@ +--- +title: Implement ability to edit hooks +merge_request: 10816 +author: Alexander Randa diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 52ba10604d4..48993420ed9 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -50,8 +50,10 @@ namespace :admin do resources :deploy_keys, only: [:index, :new, :create, :destroy] - resources :hooks, only: [:index, :create, :destroy] do - get :test + resources :hooks, only: [:index, :create, :edit, :update, :destroy] do + member do + get :test + end end resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do diff --git a/config/routes/project.rb b/config/routes/project.rb index 115ae2324b3..71fc84a7a12 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -185,7 +185,7 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do member do get :test end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 39c2a9dd1fb..0210e871a63 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :project_hook do url { generate(:url) } + enable_ssl_verification false trait :token do token { SecureRandom.hex(10) } @@ -11,6 +12,7 @@ FactoryGirl.define do merge_requests_events true tag_push_events true issues_events true + confidential_issues_events true note_events true build_events true pipeline_events true diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index fb519a9bf12..c5f24d412d7 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::Hooks", feature: true do +describe 'Admin::Hooks', feature: true do before do @project = create(:project) login_as :admin @@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do @system_hook = create(:system_hook) end - describe "GET /admin/hooks" do - it "is ok" do + describe 'GET /admin/hooks' do + it 'is ok' do visit admin_root_path - page.within ".layout-nav" do - click_on "Hooks" + page.within '.layout-nav' do + click_on 'Hooks' end expect(current_path).to eq(admin_hooks_path) end - it "has hooks list" do + it 'has hooks list' do visit admin_hooks_path expect(page).to have_content(@system_hook.url) end end - describe "New Hook" do + describe 'New Hook' do let(:url) { generate(:url) } it 'adds new hook' do @@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do end end - describe "Test" do + describe 'Update existing hook' do + let(:new_url) { generate(:url) } + + it 'updates existing hook' do + visit admin_hooks_path + + click_link 'Edit' + fill_in 'hook_url', with: new_url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(new_url) + end + end + + describe 'Remove existing hook' do + it 'remove existing hook' do + visit admin_hooks_path + + expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + end + end + + describe 'Test' do before do WebMock.stub_request(:post, @system_hook.url) visit admin_hooks_path - click_link "Test hook" + click_link 'Test hook' end it { expect(current_path).to eq(admin_hooks_path) } diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb new file mode 100644 index 00000000000..7909234556e --- /dev/null +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +feature 'Integration settings', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:role) { :developer } + let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) } + + background do + login_as(user) + project.team << [user, role] + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'to be disallowed to view' do + visit integrations_path + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Webhooks' do + let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) } + let(:url) { generate(:url) } + + scenario 'show list of webhooks' do + hook + + visit integrations_path + + expect(page.status_code).to eq(200) + expect(page).to have_content(hook.url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + expect(page).to have_content('Issues Events') + expect(page).to have_content('Confidential Issues Events') + expect(page).to have_content('Note Events') + expect(page).to have_content('Merge Requests Events') + expect(page).to have_content('Pipeline Events') + expect(page).to have_content('Wiki Page Events') + end + + scenario 'create webhook' do + visit integrations_path + + fill_in 'hook_url', with: url + check 'Tag push events' + check 'Enable SSL verification' + + click_button 'Add webhook' + + expect(page).to have_content(url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + end + + scenario 'edit existing webhook' do + hook + visit integrations_path + + click_link 'Edit' + fill_in 'hook_url', with: url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(page).to have_content(url) + end + + scenario 'test existing webhook' do + WebMock.stub_request(:post, hook.url) + visit integrations_path + + click_link 'Test' + + expect(current_path).to eq(integrations_path) + end + + scenario 'remove existing webhook' do + hook + visit integrations_path + + expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + end + end + end +end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 99c44bde151..e5fc0b676af 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do end end -# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test +# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test # admin_hooks GET /admin/hooks(.:format) admin/hooks#index # POST /admin/hooks(.:format) admin/hooks#create # admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy +# PUT /admin/hooks/:id(.:format) admin/hooks#update +# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit describe Admin::HooksController, "routing" do it "to #test" do - expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1') + expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1') end it "to #index" do @@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do expect(post("/admin/hooks")).to route_to('admin/hooks#create') end + it "to #edit" do + expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1') + end + + it "to #update" do + expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1') + end + it "to #destroy" do expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1') end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index a3de022d242..163df072cf6 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -340,14 +340,16 @@ describe 'project routing' do # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create - # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy + # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit + # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update + # DELETE /:project_id/hooks/:id(.:format) hooks#destroy describe Projects::HooksController, 'routing' do it 'to #test' do expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } + let(:actions) { [:index, :create, :destroy, :edit, :update] } let(:controller) { 'hooks' } end end From 573d0989110ccb0630481f416d1a4f8fd05ee608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Fri, 28 Apr 2017 14:12:29 +0200 Subject: [PATCH 10/22] Cleanup --- app/models/repository.rb | 4 +--- spec/models/repository_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index ce1238e66d2..2ffd9115372 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -505,10 +505,8 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - delegate :branch_count, to: :raw_repository + delegate :branch_count, :tag_count, to: :raw_repository cache_method :branch_count, fallback: 0 - - delegate :tag_count, to: :raw_repository cache_method :tag_count, fallback: 0 def avatar diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4526c3194b6..dfa211ec6cd 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1379,7 +1379,10 @@ describe Repository, models: true do describe '#branch_count' do it 'returns the number of branches' do expect(repository.branch_count).to be_an(Integer) + + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync rugged_count = repository.raw_repository.rugged.branches.count + expect(repository.branch_count).to eq(rugged_count) end end @@ -1387,8 +1390,10 @@ describe Repository, models: true do describe '#tag_count' do it 'returns the number of tags' do expect(repository.tag_count).to be_an(Integer) + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync rugged_count = repository.raw_repository.rugged.tags.count + expect(repository.tag_count).to eq(rugged_count) end end From fd482ace44025f4a748f3b87bb994383d3f96f6c Mon Sep 17 00:00:00 2001 From: TM Lee Date: Tue, 25 Apr 2017 13:50:59 +0800 Subject: [PATCH 11/22] [#31254] Change Git commit command in "Existing folder" to git commit -m --- app/views/projects/empty.html.haml | 2 +- .../31254-change-git-commit-command-in-existing-folder.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 85e442e115c..50e0bad3ccf 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -60,7 +60,7 @@ git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git add . - git commit + git commit -m "Initial commit" git push -u origin master %fieldset diff --git a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml new file mode 100644 index 00000000000..950336ea932 --- /dev/null +++ b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml @@ -0,0 +1,4 @@ +--- +title: Change Git commit command in Existing folder to git commit -m +merge_request: 10900 +author: TM Lee From 2b3fc5e624bd0c8b9e1c68bf2b3741d8898cf0b0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 30 Apr 2017 12:15:20 -0500 Subject: [PATCH 12/22] Add download button to project snippets --- app/controllers/concerns/snippets_actions.rb | 4 +- app/controllers/snippets_controller.rb | 14 +- app/helpers/blob_helper.rb | 8 +- app/views/shared/snippets/_blob.html.haml | 3 +- app/views/snippets/show.html.haml | 2 +- .../unreleased/dm-snippet-download-button.yml | 4 + config/routes/project.rb | 2 +- config/routes/snippets.rb | 3 +- spec/controllers/snippets_controller_spec.rb | 246 +++++++++--------- spec/features/projects/snippets/show_spec.rb | 12 + spec/features/snippets/show_spec.rb | 12 + 11 files changed, 162 insertions(+), 148 deletions(-) create mode 100644 changelogs/unreleased/dm-snippet-download-button.yml diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index ca6dffe1cc5..ffea712a833 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -5,10 +5,12 @@ module SnippetsActions end def raw + disposition = params[:inline] == 'false' ? 'attachment' : 'inline' + send_data( convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', - disposition: 'inline', + disposition: disposition, filename: @snippet.sanitized_file_name ) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 906833505d1..7fbfa6c2ee4 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -5,10 +5,10 @@ class SnippetsController < ApplicationController include MarkdownPreview include RendersBlob - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw, :download] + before_action :authorize_read_snippet!, only: [:show, :raw] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -16,7 +16,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html @@ -83,14 +83,6 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def download - send_data( - convert_line_endings(@snippet.content), - type: 'text/plain; charset=utf-8', - filename: @snippet.sanitized_file_name - ) - end - def preview_markdown render_markdown_preview(params[:text], skip_project_check: true) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 377b080b3c6..5a8f615fc2d 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -118,15 +118,15 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_raw_url + def blob_raw_url(params = {}) if @snippet if @snippet.project_id - raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) + raw_namespace_project_snippet_path(@project.namespace, @project, @snippet, params) else - raw_snippet_path(@snippet) + raw_snippet_path(@snippet, params) end elsif @blob - namespace_project_raw_path(@project.namespace, @project, @id) + namespace_project_raw_path(@project.namespace, @project, @id, params) end end diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 67d186e2874..fd4ee840a19 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -18,7 +18,6 @@ = copy_blob_source_button(blob) = open_raw_blob_button(blob) - - if defined?(download_path) && download_path - = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + = link_to icon('download'), blob_raw_url(inline: false), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } = render 'projects/blob/content', blob: blob diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 8a80013bbfd..ad07985951c 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 - = render 'shared/snippets/blob', download_path: download_snippet_path(@snippet) + = render 'shared/snippets/blob' .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/changelogs/unreleased/dm-snippet-download-button.yml b/changelogs/unreleased/dm-snippet-download-button.yml new file mode 100644 index 00000000000..09ece1e7f98 --- /dev/null +++ b/changelogs/unreleased/dm-snippet-download-button.yml @@ -0,0 +1,4 @@ +--- +title: Add download button to project snippets +merge_request: +author: diff --git a/config/routes/project.rb b/config/routes/project.rb index 115ae2324b3..e777b9fc6e7 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do - get 'raw' + get :raw post :mark_as_spam end end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 56534f677be..4ce4c11aa65 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -1,7 +1,6 @@ resources :snippets, concerns: :awardable do member do - get 'raw' - get 'download' + get :raw post :mark_as_spam post :preview_markdown end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 234f3edd3d8..41cd5bdcdd8 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -350,144 +350,138 @@ describe SnippetsController do end end - %w(raw download).each do |action| - describe "GET #{action}" do - context 'when the personal snippet is private' do - let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + describe "GET #raw" do + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } - context 'when signed in' do - before do - sign_in(user) - end - - context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } - - it 'responds with status 404' do - get action, id: other_personal_snippet.to_param - - expect(response).to have_http_status(404) - end - end - - context 'when signed in user is the author' do - before { get action, id: personal_snippet.to_param } - - it 'responds with status 200' do - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - - it 'has expected headers' do - expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - - if action == :download - expect(response.header['Content-Disposition']).to match(/attachment/) - elsif action == :raw - expect(response.header['Content-Disposition']).to match(/inline/) - end - end - end + context 'when signed in' do + before do + sign_in(user) end - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context 'when the personal snippet is internal' do - let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } - - context 'when signed in' do - before do - sign_in(user) - end - - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - end - - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context 'when the personal snippet is public' do - let(:personal_snippet) { create(:personal_snippet, :public, author: user) } - - context 'when signed in' do - before do - sign_in(user) - end - - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - 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 - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - end - end - - context 'when the personal snippet does not exist' do - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } it 'responds with status 404' do - get action, id: 'doesntexist' + get :raw, id: other_personal_snippet.to_param expect(response).to have_http_status(404) end end - context 'when not signed in' do - it 'responds with status 404' do - get action, id: 'doesntexist' + context 'when signed in user is the author' do + before { get :raw, id: personal_snippet.to_param } - expect(response).to have_http_status(404) + it 'responds with status 200' do + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) end + + it 'has expected headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + + expect(response.header['Content-Disposition']).to match(/inline/) + end + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + 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 :raw, 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 :raw, 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 + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end + end + end + + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response).to have_http_status(404) + end + end + + context 'when not signed in' do + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response).to have_http_status(404) end end end diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb index 7eb1210e307..cedf3778c7e 100644 --- a/spec/features/projects/snippets/show_spec.rb +++ b/spec/features/projects/snippets/show_spec.rb @@ -30,6 +30,12 @@ feature 'Project snippet', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -59,6 +65,12 @@ feature 'Project snippet', :js, feature: true do # shows a disabled copy button expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb index cebcba6a230..e36cf547f80 100644 --- a/spec/features/snippets/show_spec.rb +++ b/spec/features/snippets/show_spec.rb @@ -24,6 +24,12 @@ feature 'Snippet', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -53,6 +59,12 @@ feature 'Snippet', :js, feature: true do # shows a disabled copy button expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end From d3d3767bf1da9acd22e8013de679efc3b633493d Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Mon, 1 May 2017 12:08:25 +0800 Subject: [PATCH 13/22] Display GitLab Pages status in Admin Dashboard --- app/views/admin/dashboard/index.html.haml | 6 ++++++ ...play-whether-pages-is-enabled-in-the-admin-dashboard.yml | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8c9fdc9ae42..53f0a1e7fde 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -73,6 +73,12 @@ = container_reg %span.light.pull-right = boolean_to_icon Gitlab.config.registry.enabled + - gitlab_pages = 'GitLab Pages' + - gitlab_pages_enabled = Gitlab.config.pages.enabled + %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } + = gitlab_pages + %span.light.pull-right + = boolean_to_icon gitlab_pages_enabled .col-md-4 %h4 diff --git a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml new file mode 100644 index 00000000000..4452b13037b --- /dev/null +++ b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml @@ -0,0 +1,4 @@ +--- +title: Display GitLab Pages status in Admin Dashboard +merge_request: +author: From 6d5095332c65684474d204315f01cc165593f668 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Mon, 1 May 2017 15:49:12 +0530 Subject: [PATCH 14/22] added not-found status favicons --- .../images/ci_favicons/dev/favicon-notfound.ico | Bin 0 -> 4286 bytes .../images/ci_favicons/favicon-notfound.ico | Bin 0 -> 4286 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/assets/images/ci_favicons/dev/favicon-notfound.ico create mode 100644 app/assets/images/ci_favicons/favicon-notfound.ico diff --git a/app/assets/images/ci_favicons/dev/favicon-notfound.ico b/app/assets/images/ci_favicons/dev/favicon-notfound.ico new file mode 100644 index 0000000000000000000000000000000000000000..ed19b69e1c5bef5fc97b8d8273886f6fbbd028f2 GIT binary patch literal 4286 zcmb_fZERCz6ux8{jnSZy_>qX>pSAb40}Pq`7zz7fOhAZZ?Iy*jVM7xmKNRE##>k>E z5y*&!5E(2$*G+~AhQtuVhml0sSBEnhZp^Kt>po~#w{~S~Z$FRcxx2U2_O=_sdvp5U zd){--bKdjuzNsKA;b+AP0lz})lY;P!AP7b#W(nm?;46dI9`qohqN`W0ZoGEwS_4xr z(=>m}JdZG4X4=)=-TmT&#)ZAMwzf@dzmM^#y}dnoY{i&p0h5?vf_gphkMRNM@PpC zj4k)|_2q7w(GrOSaZM2Xo12@aoy3O(I52pDZ>89Q$?9@%#OGO``|2_!o*144^l8 zKbIGa|Hs8c-vb}M=NqUwbze#z^2vdZ^MUb47Q*jtShLjAY(CQW^KQj9YAoN##77?# z@)c(k`zja0cb!~aI^JS-`hPUh@R^Sk{Ae_q4_@K-_xGn`fIf`HV!HBb4Nm97AGVk5 zoM@J2ysZ*V9%X8@DR$963k}quRtNdc37pj&|K4o;zfP7u=W&=%`%ar^tVN`WMu}nv z3>5zMRR!PU@#wpcaXugh$PdnH)L&bha^J}u^G`M0%-L-(smB=;IIPy2hONbZ zM=rkRi*N5cN?*VaJ!=2`5_MlX-iy0C6EpL@-?6%U`nV~TZu4XE4skG5YovG$ z=US!Y`)s#XydYOd8)Rz%!g{TA9`c$l=2Wc#`nM+Q${)|h%>I@u#3R@rdQ2niIb30` z&X(2t4!>#MLRK|Tstwqaw}{8|7>2V;`KVT$#6L^z8U6*F4XA zo`ODdAGVq+_2lyLg4;@-idP!O=`ABgE#mona)tPuvvbgoTaDfcu8BgjdTWg9Ae%_C z#q@#R`a-;jRoV`Hw$r+htloOqB2_1?hG?*A<;!~O3-Q8ROP43C2ATbMdm&l9^^mQ6 znYX&^Gs55Y|2e$Yvc0}V_AQaHo>Ib6l7&Z@iYO>7A-6&TT@mgQ`;uZxFa??1Om$2= i;3w`g(9$|Vc!_B}=*m9zA@@Ev%|9a)q?z;+rGEjuZ13y< literal 0 HcmV?d00001 diff --git a/app/assets/images/ci_favicons/favicon-notfound.ico b/app/assets/images/ci_favicons/favicon-notfound.ico new file mode 100644 index 0000000000000000000000000000000000000000..49b9b232dd1a27e39b1d3c82cbf2292239402d09 GIT binary patch literal 4286 zcmb`JYiJx*6vrn-@M8thPoe?a*_mxcKS;hw)Cy^nMXY&X8`0JxQnV%2RuI7kK|yF6 zo6wY4ENv1&nt&jvpaG-J`$3e%Jj}z*rb#y0B)glH9|{>2XPt6c$!VFS0;yG#=aQMnsv-&CPi&EiKh7eJqRo z9_4<5rIF>s?(Xg<_nHfTt*xyd_CLTpYHMrL`}_N8WMqWK$H!@CXoz}xdNhpjoH+ND z9UUEy?nN9}GyA*(-(6i@+T7e6>AKEpds|&yHDcoV36@EY;n@d`f&J8gJvur{iA2H% zb1Z2j)-g#8=AoiUW7_#Q6rF;TtuUs+svX2iwJp zJBnkCrsD6fpvPR5`oD6mM)EC<&Rm}|`0MNIsqlt=aBwi01DwNHEao_Wvj!KP`0lT% z$n~wZZm?^Z|A|J&znU}r;{5E?P=lflQr#1{t2zIDR=%rTtCwr^gu%wzzhF75QTD}+ zn)Ug7j`*3G8RKk=!LhM1BVN8MgeuviPVv29ezo8~eVz8ZZfG+}F4)&L*x*Cz>bv;9 z?@sy=58-gwV6jh{ot>rSIiHSyn%(c7g% zhMnK<7ignLO=7P_?Ten*w z8lIE6LFc_M*Q%52uVVH&JKuF`Ims3=AP?9J_(V_;$TgFyCOaeSP7N z?QE=L>ajqF)rVRzUu&J`^nU1#(DT+ka32p9JIzg{a~B8S*~*_xU~f~+x9ZjWnMl(9 z$aKs)zSBG3D;a9;bX@pwduD?>*_g*&1%aQ0EplldOZQoMZSF7O1ACgT9p)V@j=Usn z4;6(@@5WZlGn$A^s16Og@5ug(zP+qD=MN)HTS-*=KCXNeY($T z;rw0Y*!=J5&Uc!}_N{8+;vJ_ktBy#@p@g)LqSC`GnG}*T$ZLp1P11&xanFVt3oe8Z rOA|{O3peBm-!sTQmZ!=jDaVjYO*u{Ey(DCUyfi@}L&6lM2(8oqz|{oQ literal 0 HcmV?d00001 From 42e0a8cfa500306f84956dc2c0f8c675cbad53ee Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 1 May 2017 14:42:37 +0100 Subject: [PATCH 15/22] Renamed notfound favicon name --- ...found.ico => favicon_status_not_found.ico} | Bin .../images/ci_favicons/favicon-notfound.ico | Bin 4286 -> 0 bytes .../ci_favicons/favicon_status_not_found.ico | Bin 5430 -> 4286 bytes 3 files changed, 0 insertions(+), 0 deletions(-) rename app/assets/images/ci_favicons/dev/{favicon-notfound.ico => favicon_status_not_found.ico} (100%) delete mode 100644 app/assets/images/ci_favicons/favicon-notfound.ico mode change 100755 => 100644 app/assets/images/ci_favicons/favicon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/dev/favicon-notfound.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico similarity index 100% rename from app/assets/images/ci_favicons/dev/favicon-notfound.ico rename to app/assets/images/ci_favicons/dev/favicon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/favicon-notfound.ico b/app/assets/images/ci_favicons/favicon-notfound.ico deleted file mode 100644 index 49b9b232dd1a27e39b1d3c82cbf2292239402d09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`JYiJx*6vrn-@M8thPoe?a*_mxcKS;hw)Cy^nMXY&X8`0JxQnV%2RuI7kK|yF6 zo6wY4ENv1&nt&jvpaG-J`$3e%Jj}z*rb#y0B)glH9|{>2XPt6c$!VFS0;yG#=aQMnsv-&CPi&EiKh7eJqRo z9_4<5rIF>s?(Xg<_nHfTt*xyd_CLTpYHMrL`}_N8WMqWK$H!@CXoz}xdNhpjoH+ND z9UUEy?nN9}GyA*(-(6i@+T7e6>AKEpds|&yHDcoV36@EY;n@d`f&J8gJvur{iA2H% zb1Z2j)-g#8=AoiUW7_#Q6rF;TtuUs+svX2iwJp zJBnkCrsD6fpvPR5`oD6mM)EC<&Rm}|`0MNIsqlt=aBwi01DwNHEao_Wvj!KP`0lT% z$n~wZZm?^Z|A|J&znU}r;{5E?P=lflQr#1{t2zIDR=%rTtCwr^gu%wzzhF75QTD}+ zn)Ug7j`*3G8RKk=!LhM1BVN8MgeuviPVv29ezo8~eVz8ZZfG+}F4)&L*x*Cz>bv;9 z?@sy=58-gwV6jh{ot>rSIiHSyn%(c7g% zhMnK<7ignLO=7P_?Ten*w z8lIE6LFc_M*Q%52uVVH&JKuF`Ims3=AP?9J_(V_;$TgFyCOaeSP7N z?QE=L>ajqF)rVRzUu&J`^nU1#(DT+ka32p9JIzg{a~B8S*~*_xU~f~+x9ZjWnMl(9 z$aKs)zSBG3D;a9;bX@pwduD?>*_g*&1%aQ0EplldOZQoMZSF7O1ACgT9p)V@j=Usn z4;6(@@5WZlGn$A^s16Og@5ug(zP+qD=MN)HTS-*=KCXNeY($T z;rw0Y*!=J5&Uc!}_N{8+;vJ_ktBy#@p@g)LqSC`GnG}*T$ZLp1P11&xanFVt3oe8Z rOA|{O3peBm-!sTQmZ!=jDaVjYO*u{Ey(DCUyfi@}L&6lM2(8oqz|{oQ diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico old mode 100755 new mode 100644 index 21afa9c72e6ee7432b988907f1d8f90b70cd02a2..49b9b232dd1a27e39b1d3c82cbf2292239402d09 GIT binary patch literal 4286 zcmb`JYiJx*6vrn-@M8thPoe?a*_mxcKS;hw)Cy^nMXY&X8`0JxQnV%2RuI7kK|yF6 zo6wY4ENv1&nt&jvpaG-J`$3e%Jj}z*rb#y0B)glH9|{>2XPt6c$!VFS0;yG#=aQMnsv-&CPi&EiKh7eJqRo z9_4<5rIF>s?(Xg<_nHfTt*xyd_CLTpYHMrL`}_N8WMqWK$H!@CXoz}xdNhpjoH+ND z9UUEy?nN9}GyA*(-(6i@+T7e6>AKEpds|&yHDcoV36@EY;n@d`f&J8gJvur{iA2H% zb1Z2j)-g#8=AoiUW7_#Q6rF;TtuUs+svX2iwJp zJBnkCrsD6fpvPR5`oD6mM)EC<&Rm}|`0MNIsqlt=aBwi01DwNHEao_Wvj!KP`0lT% z$n~wZZm?^Z|A|J&znU}r;{5E?P=lflQr#1{t2zIDR=%rTtCwr^gu%wzzhF75QTD}+ zn)Ug7j`*3G8RKk=!LhM1BVN8MgeuviPVv29ezo8~eVz8ZZfG+}F4)&L*x*Cz>bv;9 z?@sy=58-gwV6jh{ot>rSIiHSyn%(c7g% zhMnK<7ignLO=7P_?Ten*w z8lIE6LFc_M*Q%52uVVH&JKuF`Ims3=AP?9J_(V_;$TgFyCOaeSP7N z?QE=L>ajqF)rVRzUu&J`^nU1#(DT+ka32p9JIzg{a~B8S*~*_xU~f~+x9ZjWnMl(9 z$aKs)zSBG3D;a9;bX@pwduD?>*_g*&1%aQ0EplldOZQoMZSF7O1ACgT9p)V@j=Usn z4;6(@@5WZlGn$A^s16Og@5ug(zP+qD=MN)HTS-*=KCXNeY($T z;rw0Y*!=J5&Uc!}_N{8+;vJ_ktBy#@p@g)LqSC`GnG}*T$ZLp1P11&xanFVt3oe8Z rOA|{O3peBm-!sTQmZ!=jDaVjYO*u{Ey(DCUyfi@}L&6lM2(8oqz|{oQ literal 5430 zcmc(jSxaO&6vyNAF#Xv5*l+EN!#oW06=t3Uze#_Kk^TVVR_=lusHl%_h%|*rBPh7y z4i=?|3QD1clvtWMznF8mb-h*I>YgPKlAE*qPfkuwl50$j`N6z@Z=|g?pMEsvXJbrl z?eq7TjQ=HLzsQ($h2wc#nnb0cq2bq#j*jN0rlxJFH@FFrReIp}uw?_CmS{@!AoIG>Ay}gZ+-MaX-y@`{5dU~?sL0}CGyBKAY`@MK7-`?K-L44mKx^u)%k_s;}$f}-d+t$`ra^GjEdBqgHc64;q z6365p;*Uz7F=+58znA@tu4?eHTl9QYJ)NDMF7EUBd^B`le8a;YK}O}#mDCr)`Z`6iz zeqfEnR_AlD<8rSm276>=#LY|e*~`mICB?SrLZ`RkYg{}CjaYO3sx)r^m^Zvy@$+j1 zKj>P36^=)IB4aMjljy?d%v$Z1Yx?yOCc{jK(GZEe~9{(d_@KX17cBrRZQ97yZ(IrzkL z(!x0`F|2E8kB^U&iaI{sJA$ugiCfF0N?1o9N3N|V2Efob6nuA%p)2k_S zIo8;5u)O{545c5w%ft$W2BY|0U0rr#V}2L}fOY(Jkweu(&=7^o`#!js(D*$HwxdiwhMO5R3cUhRkDk3E1K zcZY8+#GlTe_T=QGqN>RLt4BucsF;7q8xs=~c4%nGoj+j!Xkq+H`!Sk-lIGu~rKJD^ zALnwpz=Y-H<$#7Ar>Ca@E!v6ZU(SFe=g;QmW(fxG7uIZGV4!5Yr>CcEK8`zoz)y1i zl5@pZC0Jpf>l#*i^J&}(Z^`+K4KfzoKe#Kz73}A{iQ@9jn=X^d#EH4FO7{=&tGR!j zpP#!s?%dp*rE+c)SLI5Q`xiE_4zdq|`)4^nl*5S|M@Q)Hmkr;0z>F5b|KIN4YK!;7 zH@`pFV|vdbHe^nT-ydoN@!(^!=Fg%rzZZ=8m^0>G#+bKhW8P33 Date: Sun, 5 Mar 2017 18:19:08 +0000 Subject: [PATCH 16/22] Remove notify slack job --- .gitlab-ci.yml | 16 ---------------- scripts/notify_slack.sh | 13 ------------- 2 files changed, 29 deletions(-) delete mode 100755 scripts/notify_slack.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d84725b202a..712c02046cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -420,22 +420,6 @@ trigger_docs: - master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee -# Notify slack in the end -notify:slack: - stage: post-test - <<: *dedicated-runner - variables: - SETUP_DB: "false" - USE_BUNDLE_INSTALL: "false" - script: - - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See " - when: on_failure - only: - - master@gitlab-org/gitlab-ce - - tags@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - tags@gitlab-org/gitlab-ee - pages: before_script: [] stage: pages diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh deleted file mode 100755 index 6b3bc563c7a..00000000000 --- a/scripts/notify_slack.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# Sends Slack notification ERROR_MSG to CHANNEL -# An env. variable CI_SLACK_WEBHOOK_URL needs to be set. - -CHANNEL=$1 -ERROR_MSG=$2 - -if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then - echo "Missing argument(s) - Use: $0 channel message" - echo "and set CI_SLACK_WEBHOOK_URL environment variable." -else - curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL" -fi \ No newline at end of file From 0c83f747ec68c08e84c59e5f1c2d4241e733a199 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 2 May 2017 10:37:57 +0100 Subject: [PATCH 17/22] Fixed PDF blob specs The PDF file is no longer included in the repo, so we need to create the PDF before running the tests --- spec/features/projects/blobs/blob_show_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 6a6f8b4f4d5..8dba2ccbafa 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -231,7 +231,7 @@ feature 'File blob', :js, feature: true do branch_name: 'master', commit_message: "Add PDF", file_path: 'files/test.pdf', - file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf')) + file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data ).execute visit_blob('files/test.pdf') From 5f33903b1ea4cf5a1fe551467151556336f17493 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 2 May 2017 10:48:06 +0100 Subject: [PATCH 18/22] [ci skip] Added changelog entry --- changelogs/unreleased/add-tanuki-ci-status-favicons.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/add-tanuki-ci-status-favicons.yml diff --git a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml new file mode 100644 index 00000000000..b60ad81947a --- /dev/null +++ b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml @@ -0,0 +1,4 @@ +--- +title: Updated CI status favicons to include the tanuki +merge_request: 10923 +author: From 2f2144590379b700299e35c73c3aa4bccfa4e7dd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 2 May 2017 10:19:48 +0000 Subject: [PATCH 19/22] Update docs on creating a project --- doc/gitlab-basics/create-project.md | 32 ++++++++++-------- .../img/create_new_project_button.png | Bin 6978 -> 3702 bytes 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 1c549844ee1..2513f4b420a 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,24 +1,28 @@ # How to create a project in GitLab -There are two ways to create a new project in GitLab. - -1. While in your dashboard, you can create a new project using the **New project** - green button or you can use the cross icon in the upper right corner next to - your avatar which is always visible. +1. In your dashboard, click the green **New project** button or use the plus + icon in the upper right corner of the navigation bar. ![Create a project](img/create_new_project_button.png) -1. From there you can see several options. +1. This opens the **New project** page. ![Project information](img/create_new_project_info.png) -1. Fill out the information: +1. Provide the following information: + - Enter the name of your project in the **Project name** field. You can't use + special characters, but you can use spaces, hyphens, underscores or even + emoji. + - If you have a project in a different repository, you can [import it] by + clicking an **Import project from** button provided this is enabled in + your GitLab instance. Ask your administrator if not. + - The **Project description (optional)** field enables you to enter a + description for your project's dashboard, which will help others + understand what your project is about. Though it's not required, it's a good + idea to fill this in. + - Changing the **Visibility Level** modifies the project's + [viewing and access rights](../public_access/public_access.md) for users. - 1. "Project name" is the name of your project (you can't use special characters, - but you can use spaces, hyphens, underscores or even emojis). - 1. The "Project description" is optional and will be shown in your project's - dashboard so others can briefly understand what your project is about. - 1. Select a [visibility level](../public_access/public_access.md). - 1. You can also [import your existing projects](../workflow/importing/README.md). +1. Click **Create project**. -1. Finally, click **Create project**. +[import it]: ../workflow/importing/README.md diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png index 8d7a69e55ed9a458bc65db3b8d8e819becab0e19..567f104880f900a5343fffa4b62c1bb5021e1f50 100644 GIT binary patch literal 3702 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 U1b=}O)c^nh07*qoM6N<$f|<7@LjV8( literal 6978 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 HCz1aFg=OC) From 8c3a03c1b9bf5c80571c9dc07ba258fa10dd61c8 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 27 Apr 2017 12:41:26 +0200 Subject: [PATCH 20/22] Display comments for personal snippets --- app/controllers/concerns/notes_actions.rb | 136 ++++++++++++ app/controllers/concerns/renders_notes.rb | 2 + .../concerns/toggle_award_emoji.rb | 3 +- app/controllers/projects/notes_controller.rb | 139 ++----------- app/controllers/snippets/notes_controller.rb | 44 ++++ app/controllers/snippets_controller.rb | 6 + app/finders/notes_finder.rb | 2 + app/helpers/award_emoji_helper.rb | 8 +- app/services/todo_service.rb | 2 +- app/views/discussions/_notes.html.haml | 2 +- app/views/projects/notes/_actions.html.haml | 44 ++++ app/views/projects/notes/_edit.html.haml | 3 + app/views/projects/notes/_note.html.haml | 101 --------- .../projects/notes/_notes_with_form.html.haml | 2 +- app/views/shared/notes/_note.html.haml | 62 ++++++ .../notes/_notes.html.haml | 4 +- app/views/snippets/notes/_actions.html.haml | 13 ++ app/views/snippets/notes/_edit.html.haml | 0 app/views/snippets/notes/_notes.html.haml | 2 + app/views/snippets/show.html.haml | 3 + .../12910-personal-snippets-notes-show.yml | 4 + config/routes/snippets.rb | 8 + .../projects/notes_controller_spec.rb | 41 ++++ .../snippets/notes_controller_spec.rb | 196 ++++++++++++++++++ spec/factories/notes.rb | 2 +- .../notes_on_personal_snippets_spec.rb | 39 ++++ spec/finders/notes_finder_spec.rb | 9 + spec/helpers/award_emoji_helper_spec.rb | 61 ++++++ 28 files changed, 703 insertions(+), 235 deletions(-) create mode 100644 app/controllers/concerns/notes_actions.rb create mode 100644 app/controllers/snippets/notes_controller.rb create mode 100644 app/views/projects/notes/_actions.html.haml create mode 100644 app/views/projects/notes/_edit.html.haml delete mode 100644 app/views/projects/notes/_note.html.haml create mode 100644 app/views/shared/notes/_note.html.haml rename app/views/{projects => shared}/notes/_notes.html.haml (53%) create mode 100644 app/views/snippets/notes/_actions.html.haml create mode 100644 app/views/snippets/notes/_edit.html.haml create mode 100644 app/views/snippets/notes/_notes.html.haml create mode 100644 changelogs/unreleased/12910-personal-snippets-notes-show.yml create mode 100644 spec/controllers/snippets/notes_controller_spec.rb create mode 100644 spec/features/snippets/notes_on_personal_snippets_spec.rb create mode 100644 spec/helpers/award_emoji_helper_spec.rb diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb new file mode 100644 index 00000000000..c32038d07bf --- /dev/null +++ b/app/controllers/concerns/notes_actions.rb @@ -0,0 +1,136 @@ +module NotesActions + include RendersNotes + extend ActiveSupport::Concern + + included do + before_action :authorize_admin_note!, only: [:update, :destroy] + end + + def index + current_fetched_at = Time.now.to_i + + notes_json = { notes: [], last_fetched_at: current_fetched_at } + + @notes = notes_finder.execute.inc_relations_for_view + @notes = prepare_notes_for_rendering(@notes) + + @notes.each do |note| + next if note.cross_reference_not_visible_for?(current_user) + + notes_json[:notes] << note_json(note) + end + + render json: notes_json + end + + def create + create_params = note_params.merge( + merge_request_diff_head_sha: params[:merge_request_diff_head_sha], + in_reply_to_discussion_id: params[:in_reply_to_discussion_id] + ) + @note = Notes::CreateService.new(project, current_user, create_params).execute + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def update + @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def destroy + if note.editable? + Notes::DestroyService.new(project, current_user).execute(note) + end + + respond_to do |format| + format.js { head :ok } + end + end + + private + + def note_json(note) + attrs = { + commands_changes: note.commands_changes + } + + if note.persisted? + attrs.merge!( + valid: true, + id: note.id, + discussion_id: note.discussion_id(noteable), + html: note_html(note), + note: note.note + ) + + discussion = note.to_discussion(noteable) + unless discussion.individual_note? + attrs.merge!( + discussion_resolvable: discussion.resolvable?, + + diff_discussion_html: diff_discussion_html(discussion), + discussion_html: discussion_html(discussion) + ) + end + else + attrs.merge!( + valid: false, + errors: note.errors + ) + end + + attrs + end + + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) + end + + def note_params + params.require(:note).permit( + :project_id, + :noteable_type, + :noteable_id, + :commit_id, + :noteable, + :type, + + :note, + :attachment, + + # LegacyDiffNote + :line_code, + + # DiffNote + :position + ) + end + + def noteable + @noteable ||= notes_finder.target + end + + def last_fetched_at + request.headers['X-Last-Fetched-At'] + end + + def notes_finder + @notes_finder ||= NotesFinder.new(project, current_user, finder_params) + end +end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index dd21066ac13..41c3114ad1e 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -10,6 +10,8 @@ module RendersNotes private def preload_max_access_for_authors(notes, project) + return nil unless project + user_ids = notes.map(&:author_id) project.team.max_member_access_for_user_ids(user_ids) end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index fbf9a026b10..ba5b7d33f87 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -22,7 +22,8 @@ module ToggleAwardEmoji def to_todoable(awardable) case awardable when Note - awardable.noteable + # we don't create todos for personal snippet comments for now + awardable.for_personal_snippet? ? nil : awardable.noteable when MergeRequest, Issue awardable when Snippet diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 405ea3c0a4f..37f51b2ebe3 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,68 +1,22 @@ class Projects::NotesController < Projects::ApplicationController - include RendersNotes + include NotesActions include ToggleAwardEmoji - # Authorize before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] - before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_resolve_note!, only: [:resolve, :unresolve] - def index - current_fetched_at = Time.now.to_i - - notes_json = { notes: [], last_fetched_at: current_fetched_at } - - @notes = notes_finder.execute.inc_relations_for_view - @notes = prepare_notes_for_rendering(@notes) - - @notes.each do |note| - next if note.cross_reference_not_visible_for?(current_user) - - notes_json[:notes] << note_json(note) - end - - render json: notes_json - end - + # + # This is a fix to make spinach feature tests passing: + # Controller actions are returned from AbstractController::Base and methods of parent classes are + # excluded in order to return only specific controller related methods. + # That is ok for the app (no :create method in ancestors) + # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors) + # + # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 + # def create - create_params = note_params.merge( - merge_request_diff_head_sha: params[:merge_request_diff_head_sha], - in_reply_to_discussion_id: params[:in_reply_to_discussion_id] - ) - @note = Notes::CreateService.new(project, current_user, create_params).execute - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def update - @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def destroy - if note.editable? - Notes::DestroyService.new(project, current_user).execute(note) - end - - respond_to do |format| - format.js { head :ok } - end + super end def delete_attachment @@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController def note_html(note) render_to_string( - "projects/notes/_note", + "shared/notes/_note", layout: false, formats: [:html], locals: { note: note } @@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController ) end - def note_json(note) - attrs = { - commands_changes: note.commands_changes - } - - if note.persisted? - attrs.merge!( - valid: true, - id: note.id, - discussion_id: note.discussion_id(noteable), - html: note_html(note), - note: note.note - ) - - discussion = note.to_discussion(noteable) - unless discussion.individual_note? - attrs.merge!( - discussion_resolvable: discussion.resolvable?, - - diff_discussion_html: diff_discussion_html(discussion), - discussion_html: discussion_html(discussion) - ) - end - else - attrs.merge!( - valid: false, - errors: note.errors - ) - end - - attrs - end - - def authorize_admin_note! - return access_denied! unless can?(current_user, :admin_note, note) + def finder_params + params.merge(last_fetched_at: last_fetched_at) end def authorize_resolve_note! return access_denied! unless can?(current_user, :resolve_note, note) end - - def note_params - params.require(:note).permit( - :project_id, - :noteable_type, - :noteable_id, - :commit_id, - :noteable, - :type, - - :note, - :attachment, - - # LegacyDiffNote - :line_code, - - # DiffNote - :position - ) - end - - def notes_finder - @notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at)) - end - - def noteable - @noteable ||= notes_finder.target - end - - def last_fetched_at - request.headers['X-Last-Fetched-At'] - end end diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb new file mode 100644 index 00000000000..3c4ddc1680d --- /dev/null +++ b/app/controllers/snippets/notes_controller.rb @@ -0,0 +1,44 @@ +class Snippets::NotesController < ApplicationController + include NotesActions + include ToggleAwardEmoji + + skip_before_action :authenticate_user!, only: [:index] + before_action :snippet + before_action :authorize_read_snippet!, only: [:show, :index, :create] + + private + + def note + @note ||= snippet.notes.find(params[:id]) + end + alias_method :awardable, :note + + def note_html(note) + render_to_string( + "shared/notes/_note", + layout: false, + formats: [:html], + locals: { note: note } + ) + end + + def project + nil + end + + def snippet + PersonalSnippet.find_by(id: params[:snippet_id]) + end + + def note_params + super.merge(noteable_id: params[:snippet_id]) + end + + def finder_params + params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet') + end + + def authorize_read_snippet! + return render_404 unless can?(current_user, :read_personal_snippet, snippet) + end +end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 906833505d1..e338db8353b 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,4 +1,5 @@ class SnippetsController < ApplicationController + include RendersNotes include ToggleAwardEmoji include SpammableActions include SnippetsActions @@ -64,6 +65,11 @@ class SnippetsController < ApplicationController blob = @snippet.blob override_max_blob_size(blob) + @noteable = @snippet + + @discussions = @snippet.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + respond_to do |format| format.html do render 'show' diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 3c499184b41..dc6a8ad1f66 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -68,6 +68,8 @@ class NotesFinder MergeRequestsFinder.new(@current_user, project_id: @project.id).execute when "snippet", "project_snippet" SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + when "personal_snippet" + PersonalSnippet.all else raise 'invalid target_type' end diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 167b09e678f..024cf38469e 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,10 +1,14 @@ module AwardEmojiHelper def toggle_award_url(awardable) - return url_for([:toggle_award_emoji, awardable]) unless @project + return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note) if awardable.is_a?(Note) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) - toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) + if awardable.for_personal_snippet? + toggle_award_emoji_snippet_note_path(awardable.noteable, awardable) + else + toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id) + end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index b6e88b0280f..8ae61694b50 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -281,7 +281,7 @@ class TodoService def attributes_for_target(target) attributes = { - project_id: target.project.id, + project_id: target&.project&.id, target_id: target.id, target_type: target.class.name, commit_id: nil diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 34789808f10..964473ee3e0 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,6 +1,6 @@ .discussion-notes %ul.notes{ data: { discussion_id: discussion.id } } - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - if current_user .discussion-reply-holder diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml new file mode 100644 index 00000000000..718b52dd82e --- /dev/null +++ b/app/views/projects/notes/_actions.html.haml @@ -0,0 +1,44 @@ +- access = note_max_access_for_user(note) +- if 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(@noteable), + ":note-id" => note.id, + ":resolved" => note.resolved?, + ":can-resolve" => can_resolve, + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{j(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}" } + + %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", + ":ref" => "'button'" } + + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - 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 js-note-delete danger' do + = icon('trash-o', class: 'danger-highlight') diff --git a/app/views/projects/notes/_edit.html.haml b/app/views/projects/notes/_edit.html.haml new file mode 100644 index 00000000000..f1e251d65b7 --- /dev/null +++ b/app/views/projects/notes/_edit.html.haml @@ -0,0 +1,3 @@ +.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{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml deleted file mode 100644 index 7afccb3900a..00000000000 --- a/app/views/projects/notes/_note.html.haml +++ /dev/null @@ -1,101 +0,0 @@ -- return unless note.author -- 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, note_id: note.id} } - .timeline-entry-inner - .timeline-icon - - if note.system - = icon_for_system_note(note) - - else - %a{ href: user_path(note.author) } - = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' - .timeline-content - .note-header - .note-header-info - %a{ href: user_path(note.author) } - %span.hidden-xs - = sanitize(note.author.name) - %span.note-headline-light - = note.author.to_reference - %span.note-headline-light - %span.note-headline-meta - - unless note.system - commented - - if note.system - %span.system-note-message - = note.redacted_note_html - %a{ href: "##{dom_id(note)}" } - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - unless note.system? - .note-actions - - access = note_max_access_for_user(note) - - if 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(@noteable), - ":note-id" => note.id, - ":resolved" => note.resolved?, - ":can-resolve" => can_resolve, - ":author-name" => "'#{j(note.author.name)}'", - "author-avatar" => note.author.avatar_url, - ":note-truncated" => "'#{j(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}" } - - %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", - ":ref" => "'button'" } - - = icon("spin spinner", "v-show" => "loading", class: 'loading') - %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg" - - - if current_user - - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') - %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') - %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile') - - - 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 js-note-delete danger' do - = icon('trash-o', class: 'danger-highlight') - .note-body{ class: note_editable ? 'js-task-list-container' : '' } - .note-text.md - = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - - 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{ 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? - = link_to note.attachment.url, target: '_blank' do - = image_tag note.attachment.url, class: 'note-image-attach' - .attachment - = link_to note.attachment.url, target: '_blank' do - = icon('paperclip') - = note.attachment_identifier - = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), - title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do - = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 90a150aa74c..555228623cc 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,5 @@ %ul#notes-list.notes.main-notes-list.timeline - = render "projects/notes/notes" + = render "shared/notes/notes" = render 'projects/notes/edit_form' diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml new file mode 100644 index 00000000000..731270d4127 --- /dev/null +++ b/app/views/shared/notes/_note.html.haml @@ -0,0 +1,62 @@ +- return unless note.author +- 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, note_id: note.id} } + .timeline-entry-inner + .timeline-icon + - if note.system + = icon_for_system_note(note) + - else + %a{ href: user_path(note.author) } + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' + .timeline-content + .note-header + .note-header-info + %a{ href: user_path(note.author) } + %span.hidden-xs + = sanitize(note.author.name) + %span.note-headline-light + = note.author.to_reference + %span.note-headline-light + %span.note-headline-meta + - unless note.system + commented + - if note.system + %span.system-note-message + = note.redacted_note_html + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + - unless note.system? + .note-actions + - if note.for_personal_snippet? + = render 'snippets/notes/actions', note: note, note_editable: note_editable + - else + = render 'projects/notes/actions', note: note, note_editable: note_editable + .note-body{ class: note_editable ? 'js-task-list-container' : '' } + .note-text.md + = note.redacted_note_html + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + - if note_editable + - if note.for_personal_snippet? + = render 'snippets/notes/edit', note: note + - else + = render 'projects/notes/edit', 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? + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' + .attachment + = link_to note.attachment.url, target: '_blank' do + = icon('paperclip') + = note.attachment_identifier + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), + title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/shared/notes/_notes.html.haml similarity index 53% rename from app/views/projects/notes/_notes.html.haml rename to app/views/shared/notes/_notes.html.haml index 2b2bab09c74..cfdfeeb9e97 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/shared/notes/_notes.html.haml @@ -1,8 +1,8 @@ - if defined?(@discussions) - @discussions.each do |discussion| - if discussion.individual_note? - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - else = render 'discussions/discussion', discussion: discussion - else - = render partial: "projects/notes/note", collection: @notes, as: :note + = render partial: "shared/notes/note", collection: @notes, as: :note diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml new file mode 100644 index 00000000000..dace11e5474 --- /dev/null +++ b/app/views/snippets/notes/_actions.html.haml @@ -0,0 +1,13 @@ +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + - if note_editable + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil', class: 'link-highlight') + = link_to snippet_note_path(note.noteable, 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') diff --git a/app/views/snippets/notes/_edit.html.haml b/app/views/snippets/notes/_edit.html.haml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/views/snippets/notes/_notes.html.haml b/app/views/snippets/notes/_notes.html.haml new file mode 100644 index 00000000000..f07d6b8c126 --- /dev/null +++ b/app/views/snippets/notes/_notes.html.haml @@ -0,0 +1,2 @@ +%ul#notes-list.notes.main-notes-list.timeline + = render "projects/notes/notes" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 8a80013bbfd..abd0d5e9b73 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,3 +7,6 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true + +%ul#notes-list.notes.main-notes-list.timeline + #notes= render 'shared/notes/notes' diff --git a/changelogs/unreleased/12910-personal-snippets-notes-show.yml b/changelogs/unreleased/12910-personal-snippets-notes-show.yml new file mode 100644 index 00000000000..15c6f3c5e6a --- /dev/null +++ b/changelogs/unreleased/12910-personal-snippets-notes-show.yml @@ -0,0 +1,4 @@ +--- +title: Display comments for personal snippets +merge_request: +author: diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 56534f677be..bced4a7975e 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -5,6 +5,14 @@ resources :snippets, concerns: :awardable do post :mark_as_spam post :preview_markdown end + + scope module: :snippets do + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + member do + delete :delete_attachment + end + end + end end get '/s/:username', to: redirect('/u/%{username}/snippets'), diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index f140eaef5d5..45f4cf9180d 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -167,6 +167,47 @@ describe Projects::NotesController do end end + describe 'DELETE destroy' do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js + } + end + + context 'user is the author of a note' do + before do + sign_in(note.author) + project.team << [note.author, :developer] + end + + it "returns status 200 for html" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0) + end + end + + context 'user is not the author of a note' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb new file mode 100644 index 00000000000..1c494b8c7ab --- /dev/null +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe Snippets::NotesController do + let(:user) { create(:user) } + + let(:private_snippet) { create(:personal_snippet, :private) } + let(:internal_snippet) { create(:personal_snippet, :internal) } + let(:public_snippet) { create(:personal_snippet, :public) } + + let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) } + let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) } + let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) } + + describe 'GET index' do + context 'when a snippet is public' do + before do + note_on_public + + get :index, { snippet_id: public_snippet } + end + + it "returns status 200" do + expect(response).to have_http_status(200) + end + + it "returns not empty array of notes" do + expect(JSON.parse(response.body)["notes"].empty?).to be_falsey + end + end + + context 'when a snippet is internal' do + before do + note_on_internal + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user logged in' do + before do + sign_in(user) + end + + it "returns status 200" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(200) + end + end + end + + context 'when a snippet is private' do + before do + note_on_private + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user other than author logged in' do + before do + sign_in(user) + end + + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when author logged in' do + before do + note_on_private + + sign_in(private_snippet.author) + end + + it "returns status 200" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(200) + end + + it "returns 1 note" do + get :index, { snippet_id: private_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(1) + end + end + end + + context 'dont show non visible notes' do + before do + note_on_public + + sign_in(user) + + expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true) + end + + it "does not return any note" do + get :index, { snippet_id: public_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(0) + end + end + end + + describe 'DELETE destroy' do + let(:request_params) do + { + snippet_id: public_snippet, + id: note_on_public, + format: :js + } + end + + context 'when user is the author of a note' do + before do + sign_in(note_on_public.author) + end + + it "returns status 200" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0) + end + + context 'system note' do + before do + expect_any_instance_of(Note).to receive(:system?).and_return(true) + end + + it "does not delete the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + context 'when user is not the author of a note' do + before do + sign_in(user) + + note_on_public + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + + it "does not update the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + describe 'POST toggle_award_emoji' do + let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) } + before do + sign_in(user) + end + + subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") } + + it "toggles the award emoji" do + expect { subject }.to change { note.award_emoji.count }.by(1) + + expect(response).to have_http_status(200) + end + + it "removes the already awarded emoji when it exists" do + note.toggle_award_emoji('thumbsup', user) # create award emoji before + + expect { subject }.to change { AwardEmoji.count }.by(-1) + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 93f4903119c..44c3186d813 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -5,7 +5,7 @@ include ActionDispatch::TestProcess FactoryGirl.define do factory :note do project factory: :empty_project - note "Note" + note { generate(:title) } author on_issue diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb new file mode 100644 index 00000000000..c646039e0b1 --- /dev/null +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Comments on personal snippets', feature: true do + let!(:user) { create(:user) } + let!(:snippet) { create(:personal_snippet, :public) } + let!(:snippet_notes) do + [ + create(:note_on_personal_snippet, noteable: snippet, author: user), + create(:note_on_personal_snippet, noteable: snippet) + ] + end + let!(:other_note) { create(:note_on_personal_snippet) } + + before do + login_as user + visit snippet_path(snippet) + end + + subject { page } + + context 'viewing the snippet detail page' do + it 'contains notes for a snippet with correct action icons' do + expect(page).to have_selector('#notes-list li', count: 2) + + # comment authored by current user + page.within("#notes-list li#note_#{snippet_notes[0].id}") do + expect(page).to have_content(snippet_notes[0].note) + expect(page).to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + + page.within("#notes-list li#note_#{snippet_notes[1].id}") do + expect(page).to have_content(snippet_notes[1].note) + expect(page).not_to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + end + end +end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 765bf44d863..ba6bbb3bce0 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -110,6 +110,15 @@ describe NotesFinder do expect(notes.count).to eq(1) end + it 'finds notes on personal snippets' do + note = create(:note_on_personal_snippet) + params = { target_type: 'personal_snippet', target_id: note.noteable_id } + + notes = described_class.new(project, user, params).execute + + expect(notes.count).to eq(1) + end + it 'raises an exception for an invalid target_type' do params[:target_type] = 'invalid' expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb new file mode 100644 index 00000000000..7dfd6a3f6b4 --- /dev/null +++ b/spec/helpers/award_emoji_helper_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe AwardEmojiHelper do + describe '.toggle_award_url' do + context 'note on personal snippet' do + let(:note) { create(:note_on_personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'note on project item' do + let(:note) { create(:note_on_project_snippet) } + + it 'returns correct url' do + @project = note.noteable.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'personal snippet' do + let(:snippet) { create(:personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{snippet.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(snippet)).to eq(expected_url) + end + end + + context 'merge request' do + let(:merge_request) { create(:merge_request) } + + it 'returns correct url' do + @project = merge_request.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(merge_request)).to eq(expected_url) + end + end + + context 'issue' do + let(:issue) { create(:issue) } + + it 'returns correct url' do + @project = issue.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(issue)).to eq(expected_url) + end + end + end +end From 0ca6ff67e438a8218ac53edd6280041d2f4b7a9c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 2 May 2017 08:53:29 -0500 Subject: [PATCH 21/22] Add download_snippet_path helper --- app/helpers/blob_helper.rb | 8 ++++---- app/helpers/snippets_helper.rb | 8 ++++++++ app/views/shared/snippets/_blob.html.haml | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 5a8f615fc2d..377b080b3c6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -118,15 +118,15 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_raw_url(params = {}) + def blob_raw_url if @snippet if @snippet.project_id - raw_namespace_project_snippet_path(@project.namespace, @project, @snippet, params) + raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) else - raw_snippet_path(@snippet, params) + raw_snippet_path(@snippet) end elsif @blob - namespace_project_raw_path(@project.namespace, @project, @id, params) + namespace_project_raw_path(@project.namespace, @project, @id) end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 979264c9421..2fd64b3441e 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,14 @@ module SnippetsHelper end end + def download_snippet_path(snippet) + if snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false) + else + raw_snippet_path(snippet, inline: false) + end + end + # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index fd4ee840a19..9bcb4544b97 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -18,6 +18,6 @@ = copy_blob_source_button(blob) = open_raw_blob_button(blob) - = link_to icon('download'), blob_raw_url(inline: false), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } = render 'projects/blob/content', blob: blob From 8906d8e2074903bf5841e021fe427191743ad847 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 2 May 2017 14:50:01 +0000 Subject: [PATCH 22/22] Revert "Shorten and improve some job names" This reverts commit f2fc716c4f00caf4d0184caf19c930e2d37bb6c8 --- .gitlab-ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 947a8679093..75e419b4223 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -303,12 +303,12 @@ ee_compat_check: script: - bundle exec rake db:migrate:reset -db:migrate:reset pg: +rake pg db:migrate:reset: <<: *db-migrate-reset <<: *use-pg <<: *except-docs -db:migrate:reset mysql: +rake mysql db:migrate:reset: <<: *db-migrate-reset <<: *use-mysql <<: *except-docs @@ -320,12 +320,12 @@ db:migrate:reset mysql: - bundle exec rake db:rollback STEP=120 - bundle exec rake db:migrate -db:rollback pg: +rake pg db:rollback: <<: *db-rollback <<: *use-pg <<: *except-docs -db:rollback mysql: +rake mysql db:rollback: <<: *db-rollback <<: *use-mysql <<: *except-docs @@ -347,17 +347,17 @@ db:rollback mysql: paths: - log/development.log -db:seed_fu pg: +rake pg db:seed_fu: <<: *db-seed_fu <<: *use-pg <<: *except-docs -db:seed_fu mysql: +rake mysql db:seed_fu: <<: *db-seed_fu <<: *use-mysql <<: *except-docs -gitlab:assets:compile: +rake gitlab:assets:compile: stage: test <<: *dedicated-runner <<: *except-docs @@ -377,7 +377,7 @@ gitlab:assets:compile: paths: - webpack-report/ -karma: +rake karma: cache: paths: - vendor/ruby @@ -443,11 +443,11 @@ bundler:audit: - . scripts/prepare_build.sh - bundle exec rake db:migrate -migration path pg: +migration pg paths: <<: *migration-paths <<: *use-pg -migration path mysql: +migration mysql paths: <<: *migration-paths <<: *use-mysql @@ -508,8 +508,8 @@ pages: <<: *dedicated-runner dependencies: - coverage - - karma - - gitlab:assets:compile + - rake karma + - rake gitlab:assets:compile - lint:javascript:report script: - mv public/ .public/