From ddd83291a9276003165cae4e5216349f6cb8ad4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 6 Oct 2016 17:07:41 +0200 Subject: [PATCH 001/109] Update VERSION to 8.13.0-rc1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index dff4cd02d5f..e7eb2945a0b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-pre +8.13.0-rc1 -- GitLab From 5847cf07575d01f0ca8331f53410ec08ef35abd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 11 Oct 2016 23:49:49 -0300 Subject: [PATCH 002/109] Update VERSION to 8.13.0-rc2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e7eb2945a0b..9b7d4aac4ad 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc1 +8.13.0-rc2 -- GitLab From 726a853cb4b8a6f9e241bb5d4fd6231e5c679b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 18 Oct 2016 19:37:55 +0200 Subject: [PATCH 003/109] Update VERSION to 8.13.0-rc3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9b7d4aac4ad..4854ca0b005 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc2 +8.13.0-rc3 -- GitLab From d550e43733a837af26b190ac79dbc59f114fdcf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Tue, 18 Oct 2016 16:56:13 +0000 Subject: [PATCH 004/109] Merge branch 'pipeline-emails' into 'master' Add a new pipeline email service ## What does this MR do? Add a new pipeline email service ## What are the relevant issue numbers? Closes #3976 ## Remaining tasks * [x] Preserve `·` and ` ` * [x] Use XHTML 1.0 * [ ] Use the same layout (`app/views/layouts/notify.html.haml`) * [ ] Digest or not (assets or public) * [x] A similar email for succeeded pipeline * [x] Plain text versions for both emails ## Screenshots (if relevant) https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6019#note_16594345 ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [x] `PipelinesEmailService` - [x] `SendPipelineNotificationService` See merge request !6019 --- CHANGELOG.md | 1 + .../gitlab-logo-full-horizontal.gif | Bin 0 -> 3654 bytes .../ci_pipeline_notif_v1/gitlab-logo.gif | Bin 0 -> 3040 bytes .../ci_pipeline_notif_v1/icon-branch-gray.gif | Bin 0 -> 663 bytes .../icon-check-green-inverted.gif | Bin 0 -> 369 bytes .../ci_pipeline_notif_v1/icon-commit-gray.gif | Bin 0 -> 278 bytes .../icon-x-red-inverted.gif | Bin 0 -> 1013 bytes .../ci_pipeline_notif_v1/icon-x-red.gif | Bin 0 -> 660 bytes app/controllers/admin/services_controller.rb | 8 +- app/controllers/projects/builds_controller.rb | 4 +- app/helpers/gitlab_routing_helper.rb | 16 ++ app/mailers/.gitkeep | 0 app/mailers/emails/pipelines.rb | 43 +++++ app/mailers/notify.rb | 1 + app/models/ci/build.rb | 23 ++- app/models/project.rb | 3 +- .../project_services/builds_email_service.rb | 2 +- .../pipelines_email_service.rb | 96 +++++++++ app/models/service.rb | 5 +- .../ci/send_pipeline_notification_service.rb | 19 ++ .../notify/pipeline_failed_email.html.haml | 177 +++++++++++++++++ .../notify/pipeline_failed_email.text.erb | 31 +++ .../notify/pipeline_success_email.html.haml | 154 +++++++++++++++ .../notify/pipeline_success_email.text.erb | 24 +++ lib/gitlab/ci/trace_reader.rb | 49 +++++ lib/tasks/.gitkeep | 0 lib/tasks/ci/.gitkeep | 0 spec/lib/gitlab/ci/trace_reader_spec.rb | 40 ++++ spec/lib/gitlab/import_export/all_models.yml | 3 +- spec/models/merge_request_spec.rb | 2 +- .../pipeline_email_service_spec.rb | 182 ++++++++++++++++++ ...send_pipeline_notification_service_spec.rb | 48 +++++ 32 files changed, 909 insertions(+), 22 deletions(-) create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif create mode 100644 app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif delete mode 100644 app/mailers/.gitkeep create mode 100644 app/mailers/emails/pipelines.rb create mode 100644 app/models/project_services/pipelines_email_service.rb create mode 100644 app/services/ci/send_pipeline_notification_service.rb create mode 100644 app/views/notify/pipeline_failed_email.html.haml create mode 100644 app/views/notify/pipeline_failed_email.text.erb create mode 100644 app/views/notify/pipeline_success_email.html.haml create mode 100644 app/views/notify/pipeline_success_email.text.erb create mode 100644 lib/gitlab/ci/trace_reader.rb delete mode 100644 lib/tasks/.gitkeep delete mode 100644 lib/tasks/ci/.gitkeep create mode 100644 spec/lib/gitlab/ci/trace_reader_spec.rb create mode 100644 spec/models/project_services/pipeline_email_service_spec.rb create mode 100644 spec/services/ci/send_pipeline_notification_service_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c75ad325f4..1bf24f5fa4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -250,6 +250,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Changed MR widget build status to pipeline status !6335 - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Enable pipeline events by default !6278 + - Add pipeline email service !6019 - Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Added go to issue boards keyboard shortcut - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif new file mode 100644 index 0000000000000000000000000000000000000000..3f4ef31947bc4a53d6b2d243d6e9f2c975c84b77 GIT binary patch literal 3654 zcmZ?wbhEHb+`{0*@STC-&b^0!mTJEAG5T|~{Asky-yLCp_9nlo&UopnHD%_~oqGq4jw)A>(%8qZ{K}6bL7FJCw~@deZP0>k(1`%`L@5_-u|7dxp3LWi+ui{qIV{-*-1Z39o@Pb zi`3t;WB;$O@BXh)pS@t!Q!m|LKfeAs+4%d}sc$>iO`Egqm8brnv)!+oi~er$dfij= zXNK;FSl!qxODx;+t>e=s?C_Y z{MWaSzi+Pow@m%;(KFBebU%ImeCFKc*KgjweEs&{3Z1v_-XA-0?){3nf7jV8S-I)? z%U4H^o%!_X(-UW{PYHS-KYe=SqP<}0x;YD1znReZas8q{%hZ40SoZbux$m!E{F$Tk zCtGdutfgnpUwIj%_x0MvKl8MHW^4cW@b2@GJ=5o`=%2da-zwt|A3lD6@$AIu3rkmR zTD)T8qbJXPUt9S3%a@-|AKicWWa9KC??QBsoH+aI^YdrVUw(h~_}!fTKZgn)I%zFm zz2&j1&hPURj-NVz;PAZQ;wpxPJxePuz6==BrPf zzG&j~#qU3S{F|rtdnj7Za(qQ`Q~l#{l||fvzHw{e){{P`xh_W_>!#mBU9&Bj`nv?-REh6lcq2H z+8g@o%j=`Z&;H+F`8r5z_QKVF^3}d1X?~xR`hTk0zq#rk=XXDH)SNhD$&&!H*Wvna zW6T~reDY=2vSlkb|5>c|Cs*x}v*xc~zZ8G6Fo1v#hydjY29Ey>|2btmHY_;U%pt55 zb7I57!|ejfUUM`G+-4YysDEQJKN@tbU%|O+%8WxXJ2RA}ofaK^>$QH0LGr078841> zDz&FciClVd@~od#GZ$}^*9?0Wfi^Cmiw9RU7N=MP{D*3B%gJk^G;Z;PDH zIi2&3h4GY;?(OW^Gx-_&cTBdWLk@u};&KT`r5EXor+(XRXE=4oy{yN3o$N(v-3V7EM=V z)a*!6Y-9a3ZJOVlilUP&G6frsXz?02PcRmlFww>CB*zpUX3q(V#~qVQ7BDl^^|-Jz zSY<4F;w)EpqFwh_>l7ym{YBsA_~aj8^x)LK^70_p{5j2>W`X|>b2Ef!G;y&qy}Z<> zqIIS4f<{F^qbIYn>w+e8t|K1<^fl`$M7tGLZ`^M(I`qX`Zi9gFX||m{pAM_IKbh2R zIK}P%Np8tZ6@wKHQ@1>DR9G!@gh6gc#lw>xw^nR8$TH1}<(#mI$?G?p&;N4c^*C2z zB(vp)*=#OG`>rQE%yAVTn%QU8FdxuZY{J0K&bj4tuknA2$?RNQPZZeJ?XtSK+HpUd z^%{?(aGe>e2_zZ%?I0NhX)@H3%KiiJp5T=&4;5Lk0(F#*j2FMkYLD~qI}2w zM-F(Ya!Kxby^r;)_8X6z65VT@og@?-wYNW6^ZA@L|K_8d94xwDF1U#I9^ia`<aZKOQ%GD;(zYsnF!s(>B@s z-D&?5W)9A)B{xf@_sKMJi{1;E*m?E;oV^$1(sUM_VAIw}_OX6-!EwSJi42BrCAlY? z7~knjZ1H3j&nV=5GU5IKj+!oW_hzB(2Jsaf9tuT_sxcj3y49s+9=h1^|DD-pwCO-@ z#!G39MI53QElFk@;uh2j=+Dx*P{(wrI^m3pwnO7P7NN~T6%F;JLibc`8W`C`jxhK> zU{|b2V0kUbClSrz_GK1xuL95ErsmykVm}V{r#tAX9KFC`Xx7kaXWAmp>v5P(Y{5P= z0cXJtJC10DJ!JMhqaX3j*u(rk!vwagTN$LS8k|kl47>GR8nO*%w}@O0;1%&X5HZ<+ zjqAZTenB2aUKO4L%tDNZ>SXi^rA@dW%dj6x{?Ndy;=(MZ#V%yOz(I0K69-SK$6=?f ziQ&^fi2iNL5U>wfnZvx$!B{PjNu<%|}5tRetaUO>pxB?l4K0RhKn&9xj z;f2kMrGk>*SIBWVe`sK+m?ZD~!@=WE05AUp2U!+IMoFau-f?M*xx+sN^Q1>O^IIsg zH?cf&IrE^!^UNFhqUvW35emB`6IA@-4HU8w_FjKT& z;p7iDCNG0V+mesn>;Ehf)0*LTWa4Fce+72wwdQPZQ>XB zwaN8Q@r+uTyAG2h_qY9V5;ycO9^Ap3I zGuo+AnFak(4IjkB8j?fbE3{e_9bldmaK*u4La*zNuR<#f+rGn5in}f}D}^<5d;VXdDDXpvF>$h#?=mixjocND92$(r z&Nc}ZT(eG=a<;m3Ty@dZ*{Tm^xE8eai#!(6c4_724!U*uSMZEd(Twcqw1AckrL2<| zG%=YxY)?^9mToxUnmD!UMpMFK*?k8bRIdpfd{K94w%DFVX`i2p>#C+!-Y9ml{_B0M z{|^&GQ>hb=z@=EBn((bv{)YRc*lZN~Q(a2>FgE3oc-MtHjuweg+?P!kSjp-;#=mqF<*|ZG>BA)cgo8(C^ER*?i?Lu7 zmavZrReiGH^?h!e!whBuoqA#ux|LNPNH3B;Astn4t2Qx0;pmJg&XkE)RQYdl+qrNs z2TbG^+R~tKG^0@^EihH|j1wcX$O(;AmYh5+pXC=BxC(b=$4DxCFEsf#p;uA8k>SaX zRXH=7MMJnIWlBWF$Yr!MDJ@_&`oX}eGJ}Paqbj@c$OG=p5pHT?g3Mw(6BuV0xG}|T zY-zKuVbZwJsP~Rz6KBc6dsjWK=J9qnhaORgUH!p{vxbo)H|L#v>;Df%Gp4fdG|J{W zd)>LI>2GwSYze>GWr?Fq1%<0(W@ItT{b&){-{5V}*ub*m#7p}>8@m4Y?qPX*L;TS6 zLYAZ}FJEbUFeo^Ed{Fk_5y!qC@p>YSoFOu%D<|D@;FMT-oA(EkczlG5A>X4WrjUl) z>RTDY*xmaWy*t=!1xz2hRJ;pcw2*0;K%?Ki2hFt)9{H7Sx@*MH*tF!3z{~X=U+pyl z*wrPJUt0vc-Y|XP?Mu_^#QZp(#O-A`V5Ql_yf~s&MeV?adXoiGT{B$g3Kg7W31Mcq z>Cz#!#-Lq4eTqPG%^d3~27Z4O9?$9D&C1UqcvtO6lCYD6mr=xo-heF&Sf_h9{g)H4 zoRD;)k#Vw4hjHA57OO1|%sLY;aes37|LgjE#v*~a#`hfBLSHcQ83veDE3of!sCzcu zrRoCH!U?=Q25Q^~S*;r~Rj&9g3bEKNz*4WE=>LJ^>{b3XOClvp8S}!zjFV)Q7UWi# zH+Fo;ZGN1&;2QI3XA4CI%hu!kFU45jgf*So$T=mUM)*XL_>mgcOYGBBEp7%ko4T`D zMaY-7+o-FiiMKR%A8u?(=Czm>m{E|ma$C#Sg=~Qb*pi-l8cnFteNn!LvBFTpB2T1E zC8d^cLT$_o1s8!v$)_Bm8Wsl>TFyGQG=FYnY5!ND1gCtLUOCTQxef3)ugDHE(q06HF2_ zIlyt%xcS*Je$fV2o3ft98{B#fju{hdk}vRIU%>L^LRZZWKk){(VgrVPWxZQhu&oNH z`6G}l&cVCyL`QN%vTp%<_Jh86H@IU8GXEI3nF}ahtl(WM5GIST6tlPoOe}Tn$0oT>16Zkk;TQ~51YG83J;JW>QOGv@}=8K7ZCwtih_*q}F8JRP5 y9iL?Olix=jxq8%E?X#SYI&k^GZ(f=$tG&bBfQ+DXRqq7_0%=>cPeU literal 0 HcmV?d00001 diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..387628f831c2ef2d14863a2e30bde7c6f3e5bd85 GIT binary patch literal 3040 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 Uz@z00*d$wa7(Be1t-xRn0D%4fzW@LL literal 0 HcmV?d00001 diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif new file mode 100644 index 0000000000000000000000000000000000000000..27a55b1d61fd88ef897d0021de7570288e5db0e6 GIT binary patch literal 369 zcmZ?wbhEHblwy!#xXQrbw!P%x=a=7p|M>d*d-9>qk3YX89_&au)ET|MIdWf<(fZUs z|Nj2__wUZfCk4l+G@M(o|JnJQA0EH|`8jHTbMc9(6{lwHdUE>Sr)P_9?Ob+ekL$ML zvXe8+HfGeGnfvVPo8&`XpMHLQ^5xa7504$U7X1JJ-*9~j166_IPZqFwIv^6{CkD2@ z1Ct6obfo%EEGeoCDVX8yRiJy${ORQ`42(;r ym9B7I)xBW7+r~{v3*EPE+Uc=-UEKbLEdl%Y9$npYBvndM;&kf8OP5m}8LR;vhn6e= literal 0 HcmV?d00001 diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif new file mode 100644 index 0000000000000000000000000000000000000000..8fe3281d2f6dc2fe80c94290a2fe73c93bfdd06e GIT binary patch literal 278 zcmZ?wbhEHblwy!#IKsd%ZQ8V}SFcW)GG*Spc^^J}m_2*;{Q2`IO`0@e!i0D4-hKc6 z{rdImU%!6cx^?U2%ao8@%;JoXV0F! zeED+niyDg?P^_qr-=bm2ZE#n2{!P}|bb)YHZ!B`cYcm6KOcR8qz*p%EP$FPoU0s_P#Z92y>} c>*D6&-Se}lfGTO`}x?#2l?Hfj$HV5<<3>RTTzAWS zP|$PTJ?s0ehhNTI{qy0|HOJID@paeTv+w70+)Hcz`{U3_Xo^PSk5>#mu9K79Uh_wo0e4}U#>^ZoXt*Gsm2 zI&|(qQSZI*d=& zpS=8f;pXr6pMKnX^5yi^zhAyx(hvXt|3AYh7!3i^LqPE-3s{p5hy>*c298S%%^Wfw z8x|aF<`CA3Ik6$3LzY)bVavlqEUhgvDI9{67#a>TIhak$D4gKh+AGamA+tk4`+QSB`6&8tR^ww5W{>YIgX{C37Xj?s|^j#~ZFyv->u6h&Ek+AKET3X%Ap&D6;eUANS7dqThx(-H;W35OH9R?3^X ze3d+`z&JmeRqE@_g9^QDCWnPW8cG*4bFin+xgoLhLFoAff>ApZnpQgsNrg0cc<rah^faaA+;mR9H$jo?3@zWo3D?@8_SYnma~ zG=pzwhFsPNzM>g?TQl_E&tLz({rGn2_LKS<->%>PbnwjMnrZhlTL1n2^Z(cHD;h!f zGuuAyI{tFX$`_MY{Qdds#iSKqFW$bb8Fo!O?C;m_pLU;oIc@cy&)=R;T>f(MimMvI ze?ES>tQmYesN}MC$iF{-|NZ`bO*{0OM(~52&g(kiw*rel?K$!3(AlSrvmRu%UDpY_ zt{L*}+Wp71)Bb+`c0aTA-}j#{r>=TZJL9@x)VIsG|Nr^(>-n4e+3nXIQg1k>UD1p9 zbnxtTgQ)K}9{m6J?`hNQzu$lU`SkUge#C>!wktYee?ESGkkx)oE98b|=GteF={$v4Lq5~p9al*jhRz+Ifgy zHO|#P`ou;yT|M4Iv8?WbO$?mpuN}X}8qD4z;dJlXA=WVMwmXIw4uxAjX*V$CddcD$ z*&U}4^yuIs!vm8PMU0x3N~;}spuo?vic^`vH;k#7!$-#;p_f5OPjoxafd@-Fh58OL z#j_IG>AQ)SDo{;>ys>Zd@aB%HYAv XR;`vTAsZN 0 + project.builds.any? end def disabled_title diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb new file mode 100644 index 00000000000..ec3c1bc85ee --- /dev/null +++ b/app/models/project_services/pipelines_email_service.rb @@ -0,0 +1,96 @@ +class PipelinesEmailService < Service + prop_accessor :recipients + boolean_accessor :add_pusher + boolean_accessor :notify_only_broken_pipelines + validates :recipients, + presence: true, + if: ->(s) { s.activated? && !s.add_pusher? } + + def initialize_properties + self.properties ||= { notify_only_broken_pipelines: true } + end + + def title + 'Pipelines emails' + end + + def description + 'Email the pipelines status to a list of recipients.' + end + + def to_param + 'pipelines_email' + end + + def supported_events + %w[pipeline] + end + + def execute(data, force: false) + return unless supported_events.include?(data[:object_kind]) + return unless force || should_pipeline_be_notified?(data) + + all_recipients = retrieve_recipients(data) + + return unless all_recipients.any? + + pipeline = Ci::Pipeline.find(data[:object_attributes][:id]) + Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients) + end + + def can_test? + project.pipelines.any? + end + + def disabled_title + 'Please setup a pipeline on your repository.' + end + + def test_data(project, user) + data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last) + data[:user] = user.hook_attrs + data + end + + def fields + [ + { type: 'textarea', + name: 'recipients', + placeholder: 'Emails separated by comma' }, + { type: 'checkbox', + name: 'add_pusher', + label: 'Add pusher to recipients list' }, + { type: 'checkbox', + name: 'notify_only_broken_pipelines' }, + ] + end + + def test(data) + result = execute(data, force: true) + + { success: true, result: result } + rescue StandardError => error + { success: false, result: error } + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end + + def retrieve_recipients(data) + all_recipients = recipients.to_s.split(',').reject(&:blank?) + + if add_pusher? && data[:user].try(:[], :email) + all_recipients << data[:user][:email] + end + + all_recipients + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 66c804f2b06..625fbc48302 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -196,12 +196,13 @@ class Service < ActiveRecord::Base end def self.available_services_names - %w( + %w[ asana assembla bamboo buildkite builds_email + pipelines_email bugzilla campfire custom_issue_tracker @@ -218,7 +219,7 @@ class Service < ActiveRecord::Base redmine slack teamcity - ) + ] end def self.create_from_template(project_id, template) diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb new file mode 100644 index 00000000000..ceb182801f7 --- /dev/null +++ b/app/services/ci/send_pipeline_notification_service.rb @@ -0,0 +1,19 @@ +module Ci + class SendPipelineNotificationService + attr_reader :pipeline + + def initialize(new_pipeline) + @pipeline = new_pipeline + end + + def execute(recipients) + email_template = "pipeline_#{pipeline.status}_email" + + return unless Notify.respond_to?(email_template) + + recipients.each do |to| + Notify.public_send(email_template, pipeline, to).deliver_later + end + end + end +end diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml new file mode 100644 index 00000000000..ec02c2e2d90 --- /dev/null +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -0,0 +1,177 @@ + +%html{lang: "en"} + %head + %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/ + %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ + %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %tbody + %tr.line + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}   + %tr.header + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %tbody + %tr.alert + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} + %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + Your pipeline has failed. + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} +   + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + = namespace_name + \/ + %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + = @project.name + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + = @pipeline.ref + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = @pipeline.short_sha + - if @merge_request + in + %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + = @merge_request.to_reference + .commit{style: "color:#5c5c5c;font-weight:300;"} + = @pipeline.git_commit_message.truncate(50) + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + - commit = @pipeline.commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + - if commit.author + %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + = commit.author.name + - else + %span + = commit.author_name + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} +   + - failed = @pipeline.statuses.latest.failed + %tr.pre-section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"} + Pipeline + %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = "\##{@pipeline.id}" + had + = failed.size + failed + = "#{'build'.pluralize(failed.size)}." + %tr.warning + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"} + Logs may contain sensitive data. Please consider before forwarding this email. + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"} + %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"} + %tbody + - failed.each do |build| + %tr.build-state + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"} + %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"} + = build.stage + %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} + %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"} + = build.name + %tr.build-log + %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"} + %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"} + = build.trace_html(last_lines: 10).html_safe + %tr.footer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %div + %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + · + %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb new file mode 100644 index 00000000000..8f8084b58e1 --- /dev/null +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -0,0 +1,31 @@ +Your pipeline has failed. + +Project: <%= @project.name %> ( <%= project_url(@project) %> ) +Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> ) +<% if @merge_request -%> +Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) +<% end -%> + +Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) +Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> +<% commit = @pipeline.commit -%> +<% if commit.author -%> +Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +<% else -%> +Commit Author: <%= commit.author_name %> +<% end -%> + +<% failed = @pipeline.statuses.latest.failed -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. + +<% failed.each do |build| -%> +Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> ) +Stage: <%= build.stage %> +Name: <%= build.name %> +Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> + +<% end -%> + +You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. +Manage all notifications: <%= profile_notifications_url %> +Help: <%= help_url %> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml new file mode 100644 index 00000000000..0fdf118c9bc --- /dev/null +++ b/app/views/notify/pipeline_success_email.html.haml @@ -0,0 +1,154 @@ + +%html{lang: "en"} + %head + %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/ + %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ + %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %tbody + %tr.line + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}   + %tr.header + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %tbody + %tr.success + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} + %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + Your pipeline has passed. + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} +   + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + = namespace_name + \/ + %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + = @project.name + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + = @pipeline.ref + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = @pipeline.short_sha + - if @merge_request + in + %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + = @merge_request.to_reference + .commit{style: "color:#5c5c5c;font-weight:300;"} + = @pipeline.git_commit_message.truncate(50) + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + - commit = @pipeline.commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + - if commit.author + %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + = commit.author.name + - else + %span + = commit.author_name + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} +   + %tr.success-message + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} + - build_count = @pipeline.statuses.latest.size + - stage_count = @pipeline.stages.size + Pipeline + %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = "\##{@pipeline.id}" + successfully completed + = "#{build_count} #{'build'.pluralize(build_count)}" + in + = "#{stage_count} #{'stage'.pluralize(stage_count)}." + %tr.footer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %div + %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + · + %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb new file mode 100644 index 00000000000..ae22d474f2c --- /dev/null +++ b/app/views/notify/pipeline_success_email.text.erb @@ -0,0 +1,24 @@ +Your pipeline has passed. + +Project: <%= @project.name %> ( <%= project_url(@project) %> ) +Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> ) +<% if @merge_request -%> +Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) +<% end -%> + +Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) +Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> +<% commit = @pipeline.commit -%> +<% if commit.author -%> +Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +<% else -%> +Commit Author: <%= commit.author_name %> +<% end -%> + +<% build_count = @pipeline.statuses.latest.size -%> +<% stage_count = @pipeline.stages.size -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. + +You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. +Manage all notifications: <%= profile_notifications_url %> +Help: <%= help_url %> diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb new file mode 100644 index 00000000000..37e51536e8f --- /dev/null +++ b/lib/gitlab/ci/trace_reader.rb @@ -0,0 +1,49 @@ +module Gitlab + module Ci + # This was inspired from: http://stackoverflow.com/a/10219411/1520132 + class TraceReader + BUFFER_SIZE = 4096 + + attr_accessor :path, :buffer_size + + def initialize(new_path, buffer_size: BUFFER_SIZE) + self.path = new_path + self.buffer_size = Integer(buffer_size) + end + + def read(last_lines: nil) + if last_lines + read_last_lines(last_lines) + else + File.read(path) + end + end + + def read_last_lines(max_lines) + File.open(path) do |file| + chunks = [] + pos = lines = 0 + max = file.size + + # We want an extra line to make sure fist line has full contents + while lines <= max_lines && pos < max + pos += buffer_size + + buf = if pos <= max + file.seek(-pos, IO::SEEK_END) + file.read(buffer_size) + else # Reached the head, read only left + file.seek(0) + file.read(buffer_size - (pos - max)) + end + + lines += buf.count("\n") + chunks.unshift(buf) + end + + chunks.join.lines.last(max_lines).join + end + end + end + end +end diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb new file mode 100644 index 00000000000..f06d78694d6 --- /dev/null +++ b/spec/lib/gitlab/ci/trace_reader_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Ci::TraceReader do + let(:path) { __FILE__ } + let(:lines) { File.readlines(path) } + let(:bytesize) { lines.sum(&:bytesize) } + + it 'returns last few lines' do + 10.times do + subject = build_subject + last_lines = random_lines + + expected = lines.last(last_lines).join + + expect(subject.read(last_lines: last_lines)).to eq(expected) + end + end + + it 'returns everything if trying to get too many lines' do + expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join) + end + + it 'raises an error if not passing an integer for last_lines' do + expect do + build_subject.read(last_lines: lines) + end.to raise_error(ArgumentError) + end + + def random_lines + Random.rand(lines.size) + 1 + end + + def random_buffer + Random.rand(bytesize) + 1 + end + + def build_subject + described_class.new(__FILE__, buffer_size: random_buffer) + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 5d5836e9bee..8fcbf12eab8 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -125,6 +125,7 @@ project: - drone_ci_service - emails_on_push_service - builds_email_service +- pipelines_email_service - irker_service - pivotaltracker_service - hipchat_service @@ -184,4 +185,4 @@ project: - project_feature award_emoji: - awardable -- user \ No newline at end of file +- user diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 91a423b670c..1acc8d748af 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -334,7 +334,7 @@ describe MergeRequest, models: true do wip_title = "WIP: #{subject.title}" expect(subject.wip_title).to eq wip_title - end + end it "does not add the WIP: prefix multiple times" do wip_title = "WIP: #{subject.title}" diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb new file mode 100644 index 00000000000..1368a2925e8 --- /dev/null +++ b/spec/models/project_services/pipeline_email_service_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe PipelinesEmailService do + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit('master').sha) + end + + let(:project) { create(:project) } + let(:recipient) { 'test@gitlab.com' } + + let(:data) do + Gitlab::DataBuilder::Pipeline.build(pipeline) + end + + before do + ActionMailer::Base.deliveries.clear + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:recipients) } + + context 'when pusher is added' do + before do + subject.add_pusher = true + end + + it { is_expected.not_to validate_presence_of(:recipients) } + end + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:recipients) } + end + end + + describe '#test_data' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + it 'builds test data' do + data = subject.test_data(project, user) + + expect(data[:object_kind]).to eq('pipeline') + end + end + + shared_examples 'sending email' do + before do + perform_enqueued_jobs do + run + end + end + + it 'sends email' do + sent_to = ActionMailer::Base.deliveries.flat_map(&:to) + expect(sent_to).to contain_exactly(recipient) + end + end + + shared_examples 'not sending email' do + before do + perform_enqueued_jobs do + run + end + end + + it 'does not send email' do + expect(ActionMailer::Base.deliveries).to be_empty + end + end + + describe '#test' do + def run + subject.test(data) + end + + before do + subject.recipients = recipient + end + + context 'when pipeline is failed' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + + context 'when pipeline is succeeded' do + before do + data[:object_attributes][:status] = 'success' + pipeline.update(status: 'success') + end + + it_behaves_like 'sending email' + end + end + + describe '#execute' do + def run + subject.execute(data) + end + + context 'with recipients' do + before do + subject.recipients = recipient + end + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + + context 'with succeeded pipeline' do + before do + data[:object_attributes][:status] = 'success' + pipeline.update(status: 'success') + end + + it_behaves_like 'not sending email' + end + + context 'with notify_only_broken_pipelines on' do + before do + subject.notify_only_broken_pipelines = true + end + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + + context 'with succeeded pipeline' do + before do + data[:object_attributes][:status] = 'success' + pipeline.update(status: 'success') + end + + it_behaves_like 'not sending email' + end + end + end + + context 'with empty recipients list' do + before do + subject.recipients = ' ,, ' + end + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'not sending email' + end + end + end +end diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb new file mode 100644 index 00000000000..288302cc94f --- /dev/null +++ b/spec/services/ci/send_pipeline_notification_service_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Ci::SendPipelineNotificationService, services: true do + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('master').sha, + user: user, + status: status) + end + + let(:project) { create(:project) } + let(:user) { create(:user) } + + subject{ described_class.new(pipeline) } + + describe '#execute' do + before do + reset_delivered_emails! + end + + shared_examples 'sending emails' do + it 'sends an email to pipeline user' do + perform_enqueued_jobs do + subject.execute([user.email]) + end + + email = ActionMailer::Base.deliveries.last + expect(email.subject).to include(email_subject) + expect(email.to).to eq([user.email]) + end + end + + context 'with success pipeline' do + let(:status) { 'success' } + let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } + + it_behaves_like 'sending emails' + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } + + it_behaves_like 'sending emails' + end + end +end -- GitLab From 1a3465e35d523e6bbf7fddda048a51a642b6eff6 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 18 Oct 2016 16:46:55 +0000 Subject: [PATCH 005/109] Merge branch '21192-retried-builds' into 'master' Fix retried builds styling #### What does this MR do? Adds background color to retried builds and an icon + tooltip on retried builds in build list #### Screenshots (if relevant) #### What are the relevant issue numbers? Closes #21192 See merge request !6109 --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/builds.scss | 13 +++++++++++-- app/assets/stylesheets/pages/pipelines.scss | 7 ++++++- app/helpers/builds_helper.rb | 8 ++++++++ app/models/concerns/has_status.rb | 1 + app/views/projects/builds/_sidebar.html.haml | 17 +++++------------ app/views/projects/ci/builds/_build.html.haml | 7 +++---- spec/features/projects/pipelines_spec.rb | 2 +- 8 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 app/helpers/builds_helper.rb diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7690d65de8e..eafe84570a8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -16,6 +16,7 @@ $white-light: #fff; $white-normal: #ededed; $white-dark: #ececec; +$gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 2fbf0cf34bf..d6a55fbd464 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -195,7 +195,7 @@ .build-job { position: relative; - .fa { + .fa-arrow-right { position: absolute; left: 15px; top: 20px; @@ -205,14 +205,23 @@ &.active { font-weight: bold; - .fa { + .fa-arrow-right { display: block; } } + &.retried { + background-color: $gray-lightest; + } + &:hover { background-color: $row-hover; } + + .fa-refresh { + font-size: 13px; + margin-left: 3px; + } } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7b71876b822..75aa44b6cea 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -73,6 +73,10 @@ border-top-width: 1px; } + .build.retried { + background-color: $gray-lightest; + } + .commit-link { .ci-status { @@ -109,7 +113,8 @@ .fa { font-size: 12px; - color: $table-text-gray; + color: $gl-text-color; + margin-left: 5px; } .commit-id { diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb new file mode 100644 index 00000000000..f3aaff9140d --- /dev/null +++ b/app/helpers/builds_helper.rb @@ -0,0 +1,8 @@ +module BuildsHelper + def sidebar_build_class(build, current_build) + build_class = '' + build_class += ' active' if build == current_build + build_class += ' retried' if build.retried? + build_class + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 9f64f76721d..ef3e73a4072 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -5,6 +5,7 @@ module HasStatus STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] COMPLETED_STATUSES = %w[success failed canceled] + ORDERED_STATUSES = %w[failed pending running canceled success skipped] class_methods do def status_sql diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 966633f1f89..b1053028279 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,5 +1,4 @@ -- builds = @build.pipeline.builds.latest.to_a -- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] +- builds = @build.pipeline.builds.to_a %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default @@ -124,9 +123,9 @@ %a.stage-item= stage .builds-container - - statuses.each do |build_status| + - HasStatus::ORDERED_STATUSES.each do |build_status| - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: ('active' if build == @build), data: {stage: build.stage}} + .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do = icon('arrow-right') = ci_icon_for_status(build.status) @@ -135,11 +134,5 @@ = build.name - else = build.id - - - if @build.retried? - %li.active - %a - Build ##{@build.id} - · - %i.fa.fa-warning - This build was retried. + - if build.retried? + %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'} diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 9248adfde80..bf157e4f64a 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -6,7 +6,7 @@ - coverage = local_assigns.fetch(:coverage, false) - allow_retry = local_assigns.fetch(:allow_retry, false) -%tr.build.commit +%tr.build.commit{class: ('retried' if retried)} %td.status - if can?(current_user, :read_build, build) = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) @@ -35,8 +35,9 @@ - if build.stuck? = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried') .label-container - if build.tags.any? @@ -47,8 +48,6 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail - - if retried - %span.label.label-warning retried - if build.manual? %span.label.label-info manual diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index 47482bc3cc9..db56a50e058 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -177,7 +177,7 @@ describe "Pipelines" do before { click_on 'Retry failed' } it { expect(page).not_to have_content('Retry failed') } - it { expect(page).to have_content('retried') } + it { expect(page).to have_selector('.retried') } end end -- GitLab From 9e2f3fd2bcfba19ceb7c05240699b29fa5de5281 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 19 Oct 2016 16:14:46 +0000 Subject: [PATCH 006/109] Merge branch '21444-pipeliens-new-mr' into 'master' Add pipelines tab to new MR #### What does this MR do? Adds pipelines tab to new MRs #### Screenshots (if relevant) ![Screen_Shot_2016-10-10_at_10.23.27_AM](/uploads/6c3f8f2be0cf9ba7cc78f6d918307ec0/Screen_Shot_2016-10-10_at_10.23.27_AM.png) ![Screen_Shot_2016-10-11_at_8.59.45_AM](/uploads/e67577d92327eafef6f04073f3d94212/Screen_Shot_2016-10-11_at_8.59.45_AM.png) #### What are the relevant issue numbers? Closes #21444 See merge request !6238 --- .../projects/merge_requests_controller.rb | 19 ++++-- app/models/merge_request.rb | 22 +++---- .../merge_requests/_new_submit.html.haml | 12 +++- .../projects/merge_requests/_show.html.haml | 2 +- spec/models/merge_request_spec.rb | 58 +++++++++++++------ 5 files changed, 75 insertions(+), 38 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a39b47b6d95..4df0648a502 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -483,13 +483,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @noteable = @merge_request @commits_count = @merge_request.commits.count - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline - if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close end + + define_pipelines_vars end # Discussion tab data is rendered on html responses of actions @@ -517,7 +516,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_widget_vars @pipeline = @merge_request.pipeline - @pipelines = [@pipeline].compact end def define_commit_vars @@ -544,6 +542,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def define_pipelines_vars + @pipelines = @merge_request.all_pipelines + + if @pipelines.any? + @pipeline = @pipelines.first + @statuses = @pipeline.statuses.relevant + end + end + def define_new_vars @noteable = @merge_request @@ -559,10 +566,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count + + define_pipelines_vars end def invalid_mr diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8c6905a442d..fedc35102ef 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -787,21 +787,21 @@ class MergeRequest < ActiveRecord::Base def all_pipelines return unless source_project - @all_pipelines ||= begin - sha = if persisted? - all_commits_sha - else - diff_head_sha - end - - source_project.pipelines.order(id: :desc). - where(sha: sha, ref: source_branch) - end + @all_pipelines ||= source_project.pipelines + .where(sha: all_commits_sha, ref: source_branch) + .order(id: :desc) end # Note that this could also return SHA from now dangling commits + # def all_commits_sha - merge_request_diffs.flat_map(&:commits_sha).uniq + if persisted? + merge_request_diffs.flat_map(&:commits_sha).uniq + elsif compare_commits + compare_commits.to_a.reverse.map(&:id) + else + [diff_head_sha] + end end def merge_commit diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index da6927879a4..9c6f562f7db 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -29,7 +29,11 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @pipeline + - if @pipelines.any? + %li.builds-tab + = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do + Pipelines + %span.badge= @pipelines.size %li.builds-tab = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds @@ -44,9 +48,11 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - - if @pipeline + - if @pipelines.any? #builds.builds.tab-pane = render "projects/merge_requests/show/builds" + #pipelines.pipelines.tab-pane + = render "projects/merge_requests/show/pipelines" .mr-loading-status = spinner @@ -59,5 +65,5 @@ :javascript var merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipeline ? 'true' : 'false'}" + buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 662463bc72b..fb4afe3bff2 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -61,7 +61,7 @@ %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do Pipelines - %span.badge= @merge_request.all_pipelines.size + %span.badge= @pipelines.size %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do Builds diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1acc8d748af..6db5e7f7d80 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -640,32 +640,56 @@ describe MergeRequest, models: true do end describe '#all_commits_sha' do - let(:all_commits_sha) do - subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq - end + context 'when merge request is persisted' do + let(:all_commits_sha) do + subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq + end - shared_examples 'returning all SHA' do - it 'returns all SHA from all merge_request_diffs' do - expect(subject.merge_request_diffs.size).to eq(2) - expect(subject.all_commits_sha).to eq(all_commits_sha) + shared_examples 'returning all SHA' do + it 'returns all SHA from all merge_request_diffs' do + expect(subject.merge_request_diffs.size).to eq(2) + expect(subject.all_commits_sha).to eq(all_commits_sha) + end end - end - context 'with a completely different branch' do - before do - subject.update(target_branch: 'v1.0.0') + context 'with a completely different branch' do + before do + subject.update(target_branch: 'v1.0.0') + end + + it_behaves_like 'returning all SHA' end - it_behaves_like 'returning all SHA' + context 'with a branch having no difference' do + before do + subject.update(target_branch: 'v1.1.0') + subject.reload # make sure commits were not cached + end + + it_behaves_like 'returning all SHA' + end end - context 'with a branch having no difference' do - before do - subject.update(target_branch: 'v1.1.0') - subject.reload # make sure commits were not cached + context 'when merge request is not persisted' do + context 'when compare commits are set in the service' do + let(:commit) { spy('commit') } + + subject do + build(:merge_request, compare_commits: [commit, commit]) + end + + it 'returns commits from compare commits temporary data' do + expect(subject.all_commits_sha).to eq [commit, commit] + end end - it_behaves_like 'returning all SHA' + context 'when compare commits are not set in the service' do + subject { build(:merge_request) } + + it 'returns array with diff head sha element only' do + expect(subject.all_commits_sha).to eq [subject.diff_head_sha] + end + end end end -- GitLab From d9601fca4e82aa98c3844aee4df99737d908ad52 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 19 Oct 2016 14:42:24 +0000 Subject: [PATCH 007/109] Merge branch 'corrected-build-page-header-username-to-full-name' into 'master' Changed build header username area to use the full name with the username as the tooltip ## What does this MR do? Changes build header username to use the full name with the username as the tooltip. ## Are there points in the code the reviewer needs to double check? ## Why was this MR needed? UI consistency ## Screenshots (if relevant) ![2016-09-08_19.24.22](/uploads/958422819ae3057f1499b2686d83e326/2016-09-08_19.24.22.gif) ## Does this MR meet the acceptance criteria? - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [ ] Added for this feature/bug - [ ] All builds are passing - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Branch has no merge conflicts with `master` (if you do - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Contributes to #21832 See merge request !6272 --- app/views/projects/builds/_user.html.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml index 2642de8021d..83f299da651 100644 --- a/app/views/projects/builds/_user.html.haml +++ b/app/views/projects/builds/_user.html.haml @@ -1,4 +1,7 @@ by %a{ href: user_path(@build.user) } - = image_tag avatar_icon(@build.user, 24), class: "avatar s24" - %strong= @build.user.to_reference + %span.hidden-xs + = image_tag avatar_icon(@build.user, 24), class: "avatar s24" + %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } } + = @build.user.name + %strong.visible-xs-inline= @build.user.to_reference -- GitLab From 52a38f907dfc0b0ae675faa90371cb7db87221bf Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 20 Oct 2016 13:52:20 +0000 Subject: [PATCH 008/109] Merge branch '22089-show-full-job-name-on-hover-on-pipeline-graph' into 'master' Add tooltip with jobs full name to pipelines graph ## What does this MR do? This MR adds a tooltip to build items in the pipelines graph as some build names are truncated. ## Are there points in the code the reviewer needs to double check? ## Why was this MR needed? Some job names that are truncated need to be fully identified to avoid confusion. ## Screenshots (if relevant) [Latest screenies here!](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6403#note_16968703). ## Does this MR meet the acceptance criteria? - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [ ] Added for this feature/bug - [ ] All builds are passing - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Branch has no merge conflicts with `master` (if you do - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #22089 See merge request !6403 --- app/assets/stylesheets/pages/pipelines.scss | 14 ++++++++++---- .../projects/ci/builds/_build_pipeline.html.haml | 4 ++-- .../commit/_pipeline_status_group.html.haml | 2 +- .../_generic_commit_status_pipeline.html.haml | 13 +++++++------ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 75aa44b6cea..1dc55538f65 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -365,10 +365,6 @@ &:hover { background-color: $gray-lighter; - - .dropdown-menu-toggle { - background-color: transparent; - } } &.playable { @@ -398,6 +394,15 @@ } } + .tooltip { + white-space: nowrap; + + .tooltip-inner { + overflow: hidden; + text-overflow: ellipsis; + } + } + .ci-status-text { width: 135px; white-space: nowrap; @@ -415,6 +420,7 @@ } .dropdown-menu-toggle { + background-color: transparent; border: none; width: auto; padding: 0; diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 017d3ff6af2..55965172d3f 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do = render_status_with_link('build', 'play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span.ci-status-icon = render_status_with_link('build', subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 5d0d5ba0262..f2d71fa6989 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,5 +1,5 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } +%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span.ci-status-icon = render_status_with_link('build', group_status) %span.ci-status-text diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 0a66d60accc..c45b73e4225 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,9 +1,10 @@ -- if subject.target_url - = link_to subject.target_url do +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } + - if subject.target_url + = link_to subject.target_url do + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name + - else %span.ci-status-icon = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name -- else - %span.ci-status-icon - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name -- GitLab From c8b2b3f7c32db873f1bebce3e3b1847ea24d235f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 20:41:04 +0000 Subject: [PATCH 009/109] Merge branch 'feature/group-level-labels' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add group level labels. * `LabelsFinder` * `Gitlab::Gfm::ReferenceRewriter` * `Banzai::Filter::LabelReferenceFilter` We'll be adding more feature that allow you to do cross-project management of issues. loses #19997 See merge request !6425 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/assets/javascripts/dispatcher.js.es6 | 2 + app/assets/stylesheets/pages/labels.scss | 23 +++- app/controllers/concerns/issuable_actions.rb | 5 + .../dashboard/labels_controller.rb | 4 +- app/controllers/groups/labels_controller.rb | 92 +++++++++++++ .../projects/boards/issues_controller.rb | 4 +- .../projects/boards/lists_controller.rb | 5 +- app/controllers/projects/issues_controller.rb | 4 +- app/controllers/projects/labels_controller.rb | 38 ++++-- .../projects/merge_requests_controller.rb | 7 +- app/finders/issuable_finder.rb | 17 ++- app/finders/labels_finder.rb | 92 +++++++++++++ app/helpers/labels_helper.rb | 82 ++++++++--- app/models/concerns/issuable.rb | 22 ++- app/models/concerns/sortable.rb | 10 +- app/models/group.rb | 1 + app/models/group_label.rb | 11 ++ app/models/issue.rb | 16 +++ app/models/label.rb | 129 ++++++++++++------ app/models/label_priority.rb | 8 ++ app/models/list.rb | 11 ++ app/models/merge_request.rb | 4 + app/models/project.rb | 12 +- app/models/project_label.rb | 34 +++++ app/models/todo.rb | 8 +- app/policies/group_label_policy.rb | 5 + app/policies/group_policy.rb | 1 + app/policies/project_label_policy.rb | 5 + app/services/boards/lists/create_service.rb | 6 +- app/services/boards/lists/generate_service.rb | 3 +- app/services/issuable_base_service.rb | 15 +- app/services/issues/move_service.rb | 8 +- app/services/labels/find_or_create_service.rb | 33 +++++ app/services/labels/transfer_service.rb | 78 +++++++++++ app/services/projects/autocomplete_service.rb | 2 +- app/services/projects/transfer_service.rb | 4 + .../slash_commands/interpret_service.rb | 6 +- app/views/groups/labels/destroy.js.haml | 2 + app/views/groups/labels/edit.html.haml | 7 + app/views/groups/labels/index.html.haml | 20 +++ app/views/groups/labels/new.html.haml | 8 ++ app/views/layouts/nav/_group.html.haml | 4 + app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/labels/_label.html.haml | 50 ------- app/views/projects/labels/destroy.js.haml | 2 +- app/views/projects/labels/edit.html.haml | 2 +- app/views/projects/labels/index.html.haml | 13 +- app/views/projects/labels/new.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- app/views/shared/_label.html.haml | 53 +++++++ app/views/shared/_label_row.html.haml | 7 +- app/views/shared/_labels_row.html.haml | 2 +- app/views/shared/issuable/_filter.html.haml | 9 +- app/views/shared/issuable/_form.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- .../labels/_form.html.haml | 4 +- config/locales/en.yml | 1 + config/routes/group.rb | 2 + .../20160919144305_add_type_to_labels.rb | 14 ++ .../20160919145149_add_group_id_to_labels.rb | 13 ++ .../20161014173530_create_label_priorities.rb | 25 ++++ ...161017125927_add_unique_index_to_labels.rb | 32 +++++ .../20161018024215_migrate_labels_priority.rb | 36 +++++ ...61018024550_remove_priority_from_labels.rb | 17 +++ db/schema.rb | 24 +++- doc/user/project/settings/import_export.md | 3 +- features/steps/project/issues/labels.rb | 2 +- lib/api/boards.rb | 4 +- lib/api/helpers.rb | 19 ++- lib/api/labels.rb | 2 +- lib/api/merge_requests.rb | 15 +- lib/banzai/filter/label_reference_filter.rb | 51 ++++++- lib/gitlab/fogbugz_import/importer.rb | 30 ++-- lib/gitlab/gfm/reference_rewriter.rb | 10 +- lib/gitlab/github_import/label_formatter.rb | 10 +- lib/gitlab/google_code_import/importer.rb | 24 ++-- lib/gitlab/import_export.rb | 2 +- lib/gitlab/import_export/attribute_cleaner.rb | 2 +- lib/gitlab/import_export/import_export.yml | 13 +- lib/gitlab/import_export/json_hash_builder.rb | 6 + .../import_export/project_tree_restorer.rb | 6 +- lib/gitlab/import_export/relation_factory.rb | 43 ++++-- lib/gitlab/issues_labels.rb | 4 +- .../projects/labels_controller_spec.rb | 61 ++++++--- spec/factories/label_priorities.rb | 7 + spec/factories/labels.rb | 18 ++- spec/factories/merge_requests.rb | 10 ++ .../import_export/test_project_export.tar.gz | Bin 1363770 -> 681774 bytes .../labels/update_prioritization_spec.rb | 100 +++++++++----- spec/finders/labels_finder_spec.rb | 69 ++++++++++ spec/fixtures/api/schemas/list.json | 2 +- spec/helpers/labels_helper_spec.rb | 27 ++-- .../filter/label_reference_filter_spec.rb | 82 +++++++++++ .../lib/gitlab/gfm/reference_rewriter_spec.rb | 26 +++- .../lib/gitlab/github_import/importer_spec.rb | 2 +- .../google_code_import/importer_spec.rb | 7 +- spec/lib/gitlab/import_export/all_models.yml | 3 + spec/lib/gitlab/import_export/project.json | 52 ++++++- .../project_tree_restorer_spec.rb | 37 ++++- .../import_export/project_tree_saver_spec.rb | 23 +++- .../import_export/safe_model_attributes.yml | 11 +- spec/models/group_label_spec.rb | 47 +++++++ spec/models/group_spec.rb | 1 + spec/models/label_priority_spec.rb | 20 +++ spec/models/label_spec.rb | 120 ++++++++-------- spec/models/project_label_spec.rb | 120 ++++++++++++++++ spec/models/project_spec.rb | 2 +- spec/requests/api/boards_spec.rb | 25 ++-- spec/requests/api/labels_spec.rb | 12 +- .../boards/lists/create_service_spec.rb | 4 + .../boards/lists/generate_service_spec.rb | 4 + spec/services/issues/create_service_spec.rb | 21 +++ .../labels/find_or_create_service_spec.rb | 51 +++++++ spec/services/labels/transfer_service_spec.rb | 56 ++++++++ .../projects/transfer_service_spec.rb | 10 ++ 116 files changed, 1991 insertions(+), 450 deletions(-) create mode 100644 app/controllers/groups/labels_controller.rb create mode 100644 app/finders/labels_finder.rb create mode 100644 app/models/group_label.rb create mode 100644 app/models/label_priority.rb create mode 100644 app/models/project_label.rb create mode 100644 app/policies/group_label_policy.rb create mode 100644 app/policies/project_label_policy.rb create mode 100644 app/services/labels/find_or_create_service.rb create mode 100644 app/services/labels/transfer_service.rb create mode 100644 app/views/groups/labels/destroy.js.haml create mode 100644 app/views/groups/labels/edit.html.haml create mode 100644 app/views/groups/labels/index.html.haml create mode 100644 app/views/groups/labels/new.html.haml delete mode 100644 app/views/projects/labels/_label.html.haml create mode 100644 app/views/shared/_label.html.haml rename app/views/{projects => shared}/labels/_form.html.haml (79%) create mode 100644 db/migrate/20160919144305_add_type_to_labels.rb create mode 100644 db/migrate/20160919145149_add_group_id_to_labels.rb create mode 100644 db/migrate/20161014173530_create_label_priorities.rb create mode 100644 db/migrate/20161017125927_add_unique_index_to_labels.rb create mode 100644 db/migrate/20161018024215_migrate_labels_priority.rb create mode 100644 db/migrate/20161018024550_remove_priority_from_labels.rb create mode 100644 spec/factories/label_priorities.rb create mode 100644 spec/finders/labels_finder_spec.rb create mode 100644 spec/models/group_label_spec.rb create mode 100644 spec/models/label_priority_spec.rb create mode 100644 spec/models/project_label_spec.rb create mode 100644 spec/services/labels/find_or_create_service_spec.rb create mode 100644 spec/services/labels/transfer_service_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf24f5fa4d..739c06baf14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup + - Add group level labels. (!6425) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Cancelled pipelines could be retried. !6927 - Updating verbiage on git basics to be more intuitive diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 73691f40c74..afc0d6f8c62 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -168,6 +168,8 @@ shortcut_handler = new ShortcutsNavigation(); new ShortcutsBlob(true); break; + case 'groups:labels:new': + case 'groups:labels:edit': case 'projects:labels:new': case 'projects:labels:edit': new Labels(); diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 9bac6d46355..397f89f501a 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -66,7 +66,21 @@ text-overflow: ellipsis; vertical-align: middle; max-width: 100%; - } + } + } + + .label-type { + display: block; + margin-bottom: 10px; + margin-left: 50px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 100px; + margin-left: 10px; + margin-bottom: 0; + vertical-align: middle; + } } .label-description { @@ -209,6 +223,13 @@ } .label-subscribe-button { + .label-subscribe-button-icon { + &[disabled] { + opacity: 0.5; + pointer-events: none; + } + } + .label-subscribe-button-loading { display: none; } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index bb32bc502e6..be86fa106f8 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -2,6 +2,7 @@ module IssuableActions extend ActiveSupport::Concern included do + before_action :labels, only: [:show, :new, :edit] before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update end @@ -25,6 +26,10 @@ module IssuableActions private + def labels + @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute + end + def authorize_destroy_issuable! unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) return access_denied! diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 2a88350a4ca..d5031da867a 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,9 +1,9 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) + labels = LabelsFinder.new(current_user).execute respond_to do |format| - format.json { render json: labels } + format.json { render json: labels.as_json(only: [:id, :title, :color]) } end end end diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb new file mode 100644 index 00000000000..29528b2cfaa --- /dev/null +++ b/app/controllers/groups/labels_controller.rb @@ -0,0 +1,92 @@ +class Groups::LabelsController < Groups::ApplicationController + before_action :label, only: [:edit, :update, :destroy] + before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] + before_action :save_previous_label_path, only: [:edit] + + respond_to :html + + def index + respond_to do |format| + format.html do + @labels = @group.labels.page(params[:page]) + end + + format.json do + available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute + render json: available_labels.as_json(only: [:id, :title, :color]) + end + end + end + + def new + @label = @group.labels.new + @previous_labels_path = previous_labels_path + end + + def create + @label = @group.labels.create(label_params) + + if @label.valid? + redirect_to group_labels_path(@group) + else + render :new + end + end + + def edit + @previous_labels_path = previous_labels_path + end + + def update + if @label.update_attributes(label_params) + redirect_back_or_group_labels_path + else + render :edit + end + end + + def destroy + @label.destroy + + respond_to do |format| + format.html do + redirect_to group_labels_path(@group), notice: 'Label was removed' + end + format.js + end + end + + protected + + def authorize_admin_labels! + return render_404 unless can?(current_user, :admin_label, @group) + end + + def authorize_read_labels! + return render_404 unless can?(current_user, :read_label, @group) + end + + def label + @label ||= @group.labels.find(params[:id]) + end + + def label_params + params.require(:label).permit(:title, :description, :color) + end + + def redirect_back_or_group_labels_path(options = {}) + redirect_to previous_labels_path, options + end + + def previous_labels_path + session.fetch(:previous_labels_path, fallback_path) + end + + def fallback_path + group_labels_path(@group) + end + + def save_previous_label_path + session[:previous_labels_path] = URI(request.referer || '').path + end +end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 71eb56aed0b..a2b01ff43dc 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -72,10 +72,10 @@ module Projects def serialize_as_json(resource) resource.as_json( + labels: true, only: [:iid, :title, :confidential], include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + assignee: { only: [:id, :name, :username], methods: [:avatar_url] } }) end end diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index 76ae41319c4..67e3c9add81 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -76,9 +76,8 @@ module Projects resource.as_json( only: [:id, :list_type, :position], methods: [:title], - include: { - label: { only: [:id, :title, :description, :color, :priority] } - }) + label: true + ) end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 96041b07647..cb649264146 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController @issues = issues_collection @issues = @issues.page(params[:page]) - @labels = @project.labels.where(title: params[:label_name]) + if params[:label_name].present? + @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute + end respond_to do |format| format.html diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index a6626df4826..4f855134368 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController before_action :module_enabled before_action :label, only: [:edit, :update, :destroy] + before_action :find_labels, only: [:index, :set_priorities, :remove_priority] before_action :authorize_read_label! - before_action :authorize_admin_labels!, only: [ - :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities - ] + before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, + :generate, :destroy, :remove_priority, + :set_priorities] respond_to :js, :html def index - @labels = @project.labels.unprioritized.page(params[:page]) - @prioritized_labels = @project.labels.prioritized + @prioritized_labels = @available_labels.prioritized(@project) + @labels = @available_labels.unprioritized(@project).page(params[:page]) respond_to do |format| format.html format.json do - render json: @project.labels + render json: @available_labels.as_json(only: [:id, :title, :color]) end end end @@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController end else respond_to do |format| - format.html { render 'new' } + format.html { render :new } format.json { render json: { message: @label.errors.messages }, status: 400 } end end @@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController if @label.update_attributes(label_params) redirect_to namespace_project_labels_path(@project.namespace, @project) else - render 'edit' + render :edit end end @@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController def destroy @label.destroy + @labels = find_labels respond_to do |format| format.html do @@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController def remove_priority respond_to do |format| - if label.update_attribute(:priority, nil) + label = @available_labels.find(params[:id]) + + if label.unprioritize!(project) format.json { render json: label } else - message = label.errors.full_messages.uniq.join('. ') - format.json { render json: { message: message }, status: :unprocessable_entity } + format.json { head :unprocessable_entity } end end end def set_priorities Label.transaction do - params[:label_ids].each_with_index do |label_id, index| - label = @project.labels.find_by_id(label_id) - label.update_attribute(:priority, index) if label + available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id) + label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) } + + label_ids.each_with_index do |label_id, index| + label = @available_labels.find(label_id) + label.prioritize!(project, index) end end @@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController end alias_method :subscribable_resource, :label + def find_labels + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities) + end + def authorize_admin_labels! return render_404 unless can?(current_user, :admin_label, @project) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4df0648a502..0c7411bb61d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @labels = @project.labels.where(title: params[:label_name]) + if params[:label_name].present? + labels_params = { project_id: @project.id, title: params[:label_name] } + @labels = LabelsFinder.new(current_user, labels_params).execute + end respond_to do |format| format.html @@ -569,6 +572,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + define_pipelines_vars end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9f170428100..e27986ef95b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -124,15 +124,12 @@ class IssuableFinder def labels return @labels if defined?(@labels) - if labels? && !filter_by_no_label? - @labels = Label.where(title: label_names) - - if projects - @labels = @labels.where(project: projects) + @labels = + if labels? && !filter_by_no_label? + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute + else + Label.none end - else - @labels = Label.none - end end def assignee? @@ -274,8 +271,10 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names, params[:sort]) + if projects - items = items.where(labels: { project_id: projects }) + label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id) + items = items.where(labels: { id: label_ids }) end end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb new file mode 100644 index 00000000000..6ace14a4bb5 --- /dev/null +++ b/app/finders/labels_finder.rb @@ -0,0 +1,92 @@ +class LabelsFinder < UnionFinder + def initialize(current_user, params = {}) + @current_user = current_user + @params = params + end + + def execute(authorized_only: true) + @authorized_only = authorized_only + + items = find_union(label_ids, Label) + items = with_title(items) + sort(items) + end + + private + + attr_reader :current_user, :params, :authorized_only + + def label_ids + label_ids = [] + + if project + label_ids << project.group.labels if project.group.present? + label_ids << project.labels + else + label_ids << Label.where(group_id: projects.group_ids) + label_ids << Label.where(project_id: projects.select(:id)) + end + + label_ids + end + + def sort(items) + items.reorder(title: :asc) + end + + def with_title(items) + items = items.where(title: title) if title + items + end + + def group_id + params[:group_id].presence + end + + def project_id + params[:project_id].presence + end + + def projects_ids + params[:project_ids].presence + end + + def title + params[:title].presence || params[:name].presence + end + + def project + return @project if defined?(@project) + + if project_id + @project = find_project + else + @project = nil + end + + @project + end + + def find_project + if authorized_only + available_projects.find_by(id: project_id) + else + Project.find_by(id: project_id) + end + end + + def projects + return @projects if defined?(@projects) + + @projects = authorized_only ? available_projects : Project.all + @projects = @projects.in_namespace(group_id) if group_id + @projects = @projects.where(id: projects_ids) if projects_ids + @projects = @projects.reorder(nil) + + @projects + end + + def available_projects + @available_projects ||= ProjectsFinder.new.execute(current_user) + end +end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index b9f3d6c75c2..221a84b042f 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -4,9 +4,8 @@ module LabelsHelper # Link to a Label # # label - Label object to link to - # project - Project object which will be used as the context for the label's - # link. If omitted, defaults to `@project`, or the label's own - # project. + # subject - Project/Group object which will be used as the context for the + # label's link. If omitted, defaults to the label's own group/project. # type - The type of item the link will point to (:issue or # :merge_request). If omitted, defaults to :issue. # block - An optional block that will be passed to `link_to`, forming the @@ -15,15 +14,14 @@ module LabelsHelper # # Examples: # - # # Allow the generated link to use the label's own project + # # Allow the generated link to use the label's own subject # link_to_label(label) # - # # Force the generated link to use @project - # @project = Project.first - # link_to_label(label) + # # Force the generated link to use a provided group + # link_to_label(label, subject: Group.last) # # # Force the generated link to use a provided project - # link_to_label(label, project: Project.last) + # link_to_label(label, subject: Project.last) # # # Force the generated link to point to merge requests instead of issues # link_to_label(label, type: :merge_request) @@ -32,9 +30,8 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) - project ||= @project || label.project - link = label_filter_path(project, label, type: type) + def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block) + link = label_filter_path(subject || label.subject, label, type: type) if block_given? link_to link, class: css_class, &block @@ -43,15 +40,40 @@ module LabelsHelper end end - def label_filter_path(project, label, type: issue) - send("namespace_project_#{type.to_s.pluralize}_path", - project.namespace, - project, - label_name: [label.name]) + def label_filter_path(subject, label, type: :issue) + case subject + when Group + send("#{type.to_s.pluralize}_group_path", + subject, + label_name: [label.name]) + when Project + send("namespace_project_#{type.to_s.pluralize}_path", + subject.namespace, + subject, + label_name: [label.name]) + end + end + + def edit_label_path(label) + case label + when GroupLabel then edit_group_label_path(label.group, label) + when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label) + end + end + + def destroy_label_path(label) + case label + when GroupLabel then group_label_path(label.group, label) + when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label) + end end - def project_label_names - @project.labels.pluck(:title) + def toggle_subscription_data(label) + return unless label.is_a?(ProjectLabel) + + { + url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label) + } end def render_colored_label(label, label_suffix = '', tooltip: true) @@ -68,8 +90,8 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, tooltip: true) - label_suffix = label.project.name_with_namespace + def render_colored_cross_project_label(label, source_project = nil, tooltip: true) + label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace label_suffix = " in #{escape_once(label_suffix)}" render_colored_label(label, label_suffix, tooltip: tooltip) end @@ -115,7 +137,10 @@ module LabelsHelper end def labels_filter_path + return group_labels_path(@group, :json) if @group + project = @target_project || @project + if project namespace_project_labels_path(project.namespace, project, :json) else @@ -124,11 +149,24 @@ module LabelsHelper end def label_subscription_status(label) - label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + case label + when GroupLabel then 'Subscribing to group labels is currently not supported.' + when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + end end def label_subscription_toggle_button_text(label) - label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + case label + when GroupLabel then 'Subscribing to group labels is currently not supported.' + when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + end + end + + def label_deletion_confirm_text(label) + case label + when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?' + when ProjectLabel then 'Remove this label? Are you sure?' + end end # Required for Banzai::Filter::LabelReferenceFilter diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c4b42ad82c7..17c3b526c97 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -145,8 +145,14 @@ module Issuable end def order_labels_priority(excluded_labels: []) - condition_field = "#{table_name}.id" - highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql + params = { + target_type: name, + target_column: "#{table_name}.id", + project_column: "#{table_name}.#{project_foreign_key}", + excluded_labels: excluded_labels + } + + highest_priority = highest_label_priority(params).to_sql select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). group(arel_table[:id]). @@ -230,18 +236,6 @@ module Issuable labels.order('title ASC').pluck(:title) end - def remove_labels - labels.delete_all - end - - def add_labels_by_names(label_names) - label_names.each do |label_name| - label = project.labels.create_with(color: Label::DEFAULT_COLOR). - find_or_create_by(title: label_name.strip) - self.labels << label - end - end - # Convert this Issuable class name to a format usable by Ability definitions # # Examples: diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 1ebecd86af9..12b23f00769 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -38,11 +38,13 @@ module Sortable private - def highest_label_priority(object_types, condition_field, excluded_labels: []) - query = Label.select(Label.arel_table[:priority].minimum). + def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: []) + query = Label.select(LabelPriority.arel_table[:priority].minimum). + left_join_priorities. joins(:label_links). - where(label_links: { target_type: object_types }). - where("label_links.target_id = #{condition_field}"). + where("label_priorities.project_id = #{project_column}"). + where(label_links: { target_type: target_type }). + where("label_links.target_id = #{target_column}"). reorder(nil) query.where.not(title: excluded_labels) if excluded_labels.present? diff --git a/app/models/group.rb b/app/models/group.rb index a2f88cca828..00a595d2705 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -19,6 +19,7 @@ class Group < Namespace has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source + has_many :labels, class_name: 'GroupLabel' validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/group_label.rb b/app/models/group_label.rb new file mode 100644 index 00000000000..a698b532d19 --- /dev/null +++ b/app/models/group_label.rb @@ -0,0 +1,11 @@ +class GroupLabel < Label + belongs_to :group + + validates :group, presence: true + + alias_attribute :subject, :group + + def to_reference(source_project = nil, target_project = nil, format: :id) + super(source_project, target_project, format: format) + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index abd58e0454a..133a5993815 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end + def self.project_foreign_key + 'project_id' + end + def self.sort(method, excluded_labels: []) case method.to_s when 'due_date_asc' then order_due_date_asc @@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base def check_for_spam? project.public? end + + def as_json(options = {}) + super(options).tap do |json| + if options.has_key?(:labels) + json[:labels] = labels.as_json( + project: project, + only: [:id, :title, :description, :color], + methods: [:text_color] + ) + end + end + end end diff --git a/app/models/label.rb b/app/models/label.rb index e8e12e2904e..149fd98ecb3 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -15,34 +15,49 @@ class Label < ActiveRecord::Base default_value_for :color, DEFAULT_COLOR - belongs_to :project - has_many :lists, dependent: :destroy + has_many :priorities, class_name: 'LabelPriority' has_many :label_links, dependent: :destroy has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest' validates :color, color: true, allow_blank: false - validates :project, presence: true, unless: Proc.new { |service| service.template? } # Don't allow ',' for label titles - validates :title, - presence: true, - format: { with: /\A[^,]+\z/ }, - uniqueness: { scope: :project_id } - - before_save :nullify_priority + validates :title, presence: true, format: { with: /\A[^,]+\z/ } + validates :title, uniqueness: { scope: [:group_id, :project_id] } default_scope { order(title: :asc) } - scope :templates, -> { where(template: true) } + scope :templates, -> { where(template: true) } + scope :with_title, ->(title) { where(title: title) } + + def self.prioritized(project) + joins(:priorities) + .where(label_priorities: { project_id: project }) + .reorder('label_priorities.priority ASC, labels.title ASC') + end + + def self.unprioritized(project) + labels = Label.arel_table + priorities = LabelPriority.arel_table + + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). + on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))). + join_sources - def self.prioritized - where.not(priority: nil).reorder(:priority, :title) + joins(label_priorities).where(priorities[:priority].eq(nil)) end - def self.unprioritized - where(priority: nil) + def self.left_join_priorities + labels = Label.arel_table + priorities = LabelPriority.arel_table + + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). + on(labels[:id].eq(priorities[:label_id])). + join_sources + + joins(label_priorities) end alias_attribute :name, :title @@ -77,6 +92,44 @@ class Label < ActiveRecord::Base nil end + def open_issues_count(user = nil, project = nil) + issues_count(user, project_id: project.try(:id) || project_id, state: 'opened') + end + + def closed_issues_count(user = nil, project = nil) + issues_count(user, project_id: project.try(:id) || project_id, state: 'closed') + end + + def open_merge_requests_count(user = nil, project = nil) + merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened') + end + + def prioritize!(project, value) + label_priority = priorities.find_or_initialize_by(project_id: project.id) + label_priority.priority = value + label_priority.save! + end + + def unprioritize!(project) + priorities.where(project: project).delete_all + end + + def priority(project) + priorities.find_by(project: project).try(:priority) + end + + def template? + template + end + + def text_color + LabelsHelper.text_color_for_bg(self.color) + end + + def title=(value) + write_attribute(:title, sanitize_title(value)) if value.present? + end + ## # Returns the String necessary to reference this Label in Markdown # @@ -84,49 +137,47 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" # # Returns a String # - def to_reference(from_project = nil, format: :id) + def to_reference(source_project = nil, target_project = nil, format: :id) format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference + if cross_project_reference?(source_project, target_project) + source_project.to_reference + reference else reference end end - def open_issues_count(user = nil) - issues.visible_to_user(user).opened.count - end - - def closed_issues_count(user = nil) - issues.visible_to_user(user).closed.count + def as_json(options = {}) + super(options).tap do |json| + json[:priority] = priority(options[:project]) if options.has_key?(:project) + end end - def open_merge_requests_count - merge_requests.opened.count - end + private - def template? - template + def cross_project_reference?(source_project, target_project) + source_project && target_project && source_project != target_project end - def text_color - LabelsHelper::text_color_for_bg(self.color) + def issues_count(user, params = {}) + IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) + .execute + .count end - def title=(value) - write_attribute(:title, sanitize_title(value)) if value.present? + def merge_requests_count(user, params = {}) + MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) + .execute + .count end - private - def label_format_reference(format = :id) raise StandardError, 'Unknown format' unless [:id, :name].include?(format) @@ -137,10 +188,6 @@ class Label < ActiveRecord::Base end end - def nullify_priority - self.priority = nil if priority.blank? - end - def sanitize_title(value) CGI.unescapeHTML(Sanitize.clean(value.to_s)) end diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb new file mode 100644 index 00000000000..5b85e0b6533 --- /dev/null +++ b/app/models/label_priority.rb @@ -0,0 +1,8 @@ +class LabelPriority < ActiveRecord::Base + belongs_to :project + belongs_to :label + + validates :project, :label, :priority, presence: true + validates :label_id, uniqueness: { scope: :project_id } + validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 } +end diff --git a/app/models/list.rb b/app/models/list.rb index eb87decdbc8..065d75bd1dc 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -26,6 +26,17 @@ class List < ActiveRecord::Base label? ? label.name : list_type.humanize end + def as_json(options = {}) + super(options).tap do |json| + if options.has_key?(:label) + json[:label] = label.as_json( + project: board.project, + only: [:id, :title, :description, :color] + ) + end + end + end + private def can_be_destroyed diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fedc35102ef..0cc0b3c2a0e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end + def self.project_foreign_key + 'target_project_id' + end + # Returns all the merge requests from an ActiveRecord:Relation. # # This method uses a UNION as it usually operates on the result of diff --git a/app/models/project.rb b/app/models/project.rb index aee74c3dba1..852c345c9b9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -107,7 +107,7 @@ class Project < ActiveRecord::Base # Merge requests from source project should be kept when source project was removed has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :issues, dependent: :destroy - has_many :labels, dependent: :destroy + has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy has_many :events, dependent: :destroy has_many :milestones, dependent: :destroy @@ -388,6 +388,10 @@ class Project < ActiveRecord::Base Project.count end end + + def group_ids + joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id) + end end def lfs_enabled? @@ -729,10 +733,8 @@ class Project < ActiveRecord::Base def create_labels Label.templates.each do |label| - label = label.dup - label.template = nil - label.project_id = self.id - label.save + params = label.attributes.except('id', 'template', 'created_at', 'updated_at') + Labels::FindOrCreateService.new(owner, self, params).execute end end diff --git a/app/models/project_label.rb b/app/models/project_label.rb new file mode 100644 index 00000000000..33c2b617715 --- /dev/null +++ b/app/models/project_label.rb @@ -0,0 +1,34 @@ +class ProjectLabel < Label + MAX_NUMBER_OF_PRIORITIES = 1 + + belongs_to :project + + validates :project, presence: true + + validate :permitted_numbers_of_priorities + validate :title_must_not_exist_at_group_level + + delegate :group, to: :project, allow_nil: true + + alias_attribute :subject, :project + + def to_reference(target_project = nil, format: :id) + super(project, target_project, format: format) + end + + private + + def title_must_not_exist_at_group_level + return unless group.present? && title_changed? + + if group.labels.with_title(self.title).exists? + errors.add(:title, :label_already_exists_at_group_level, group: group.name) + end + end + + def permitted_numbers_of_priorities + if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES + errors.add(:priorities, 'Number of permitted priorities exceeded') + end + end +end diff --git a/app/models/todo.rb b/app/models/todo.rb index 6ae9956ade5..11c072dd000 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base # Todos with highest priority first then oldest todos # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" def order_by_labels_priority - highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql + params = { + target_type: ['Issue', 'MergeRequest'], + target_column: "todos.target_id", + project_column: "todos.project_id" + } + + highest_priority = highest_label_priority(params).to_sql select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb new file mode 100644 index 00000000000..7b34aa182eb --- /dev/null +++ b/app/policies/group_label_policy.rb @@ -0,0 +1,5 @@ +class GroupLabelPolicy < BasePolicy + def rules + delegate! @subject.group + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 97ff6233968..b65fb68cd88 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy if master can! :create_projects can! :admin_milestones + can! :admin_label end # Only group owner and administrators can admin group diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb new file mode 100644 index 00000000000..b12b4c5166b --- /dev/null +++ b/app/policies/project_label_policy.rb @@ -0,0 +1,5 @@ +class ProjectLabelPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index abc7aeece39..fe0d762ccd2 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -3,7 +3,7 @@ module Boards class CreateService < BaseService def execute(board) List.transaction do - label = project.labels.find(params[:label_id]) + label = available_labels.find(params[:label_id]) position = next_position(board) create_list(board, label, position) @@ -12,6 +12,10 @@ module Boards private + def available_labels + LabelsFinder.new(current_user, project_id: project.id).execute + end + def next_position(board) max_position = board.lists.movable.maximum(:position) max_position.nil? ? 0 : max_position.succ diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index d8048f1c67e..939f9bfd068 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -19,8 +19,7 @@ module Boards end def find_or_create_label(params) - project.labels.create_with(color: params[:color]) - .find_or_create_by(name: params[:name]) + ::Labels::FindOrCreateService.new(current_user, project, params).execute end def label_params diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 57d521f2fea..bb92cd80cc9 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -80,17 +80,18 @@ class IssuableBaseService < BaseService def filter_labels_in_param(key) return if params[key].to_a.empty? - params[key] = project.labels.where(id: params[key]).pluck(:id) + params[key] = available_labels.where(id: params[key]).pluck(:id) end def find_or_create_label_ids labels = params.delete(:labels) return unless labels - params[:label_ids] = labels.split(",").map do |label_name| - project.labels.create_with(color: Label::DEFAULT_COLOR) - .find_or_create_by(title: label_name.strip) - .id + params[:label_ids] = labels.split(',').map do |label_name| + service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) + label = service.execute + + label.id end end @@ -111,6 +112,10 @@ class IssuableBaseService < BaseService new_label_ids end + def available_labels + LabelsFinder.new(current_user, project_id: @project.id).execute + end + def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index ab667456db7..a2a5f57d069 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -52,8 +52,12 @@ module Issues end def cloneable_label_ids - @new_project.labels - .where(title: @old_issue.labels.pluck(:title)).pluck(:id) + params = { + project_id: @new_project.id, + title: @old_issue.labels.pluck(:title) + } + + LabelsFinder.new(current_user, params).execute.pluck(:id) end def cloneable_milestone_id diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb new file mode 100644 index 00000000000..74291312c4e --- /dev/null +++ b/app/services/labels/find_or_create_service.rb @@ -0,0 +1,33 @@ +module Labels + class FindOrCreateService + def initialize(current_user, project, params = {}) + @current_user = current_user + @group = project.group + @project = project + @params = params.dup + end + + def execute + find_or_create_label + end + + private + + attr_reader :current_user, :group, :project, :params + + def available_labels + @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute + end + + def find_or_create_label + new_label = available_labels.find_by(title: title) + new_label ||= project.labels.create(params) + + new_label + end + + def title + params[:title] || params[:name] + end + end +end diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb new file mode 100644 index 00000000000..514679ed29d --- /dev/null +++ b/app/services/labels/transfer_service.rb @@ -0,0 +1,78 @@ +# Labels::TransferService class +# +# User for recreate the missing group labels at project level +# +module Labels + class TransferService + def initialize(current_user, old_group, project) + @current_user = current_user + @old_group = old_group + @project = project + end + + def execute + return unless old_group.present? + + Label.transaction do + labels_to_transfer.find_each do |label| + new_label_id = find_or_create_label!(label) + + next if new_label_id == label.id + + update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id) + update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id) + update_label_priorities(old_label_id: label.id, new_label_id: new_label_id) + end + end + end + + private + + attr_reader :current_user, :old_group, :project + + def labels_to_transfer + label_ids = [] + label_ids << group_labels_applied_to_issues.select(:id) + label_ids << group_labels_applied_to_merge_requests.select(:id) + + union = Gitlab::SQL::Union.new(label_ids) + + Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq + end + + def group_labels_applied_to_issues + Label.joins(:issues). + where( + issues: { project_id: project.id }, + labels: { type: 'GroupLabel', group_id: old_group.id } + ) + end + + def group_labels_applied_to_merge_requests + Label.joins(:merge_requests). + where( + merge_requests: { target_project_id: project.id }, + labels: { type: 'GroupLabel', group_id: old_group.id } + ) + end + + def find_or_create_label!(label) + params = label.attributes.slice('title', 'description', 'color') + new_label = FindOrCreateService.new(current_user, project, params).execute + + new_label.id + end + + def update_label_links(labels, old_label_id:, new_label_id:) + LabelLink.joins(:label). + merge(labels). + where(label_id: old_label_id). + update_all(label_id: new_label_id) + end + + def update_label_priorities(old_label_id:, new_label_id:) + LabelPriority.where(project_id: project.id, label_id: old_label_id). + update_all(label_id: new_label_id) + end + end +end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index f578f8dbea2..015f2828921 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -13,7 +13,7 @@ module Projects end def labels - @project.labels.select([:title, :color]) + LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color]) end def commands(noteable, type) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index bc7f8bf433b..28470f59807 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -28,6 +28,7 @@ module Projects Project.transaction do old_path = project.path_with_namespace old_namespace = project.namespace + old_group = project.group new_path = File.join(new_namespace.try(:path) || '', project.path) if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? @@ -57,6 +58,9 @@ module Projects # Move wiki repo also if present gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") + # Move missing group labels to project + Labels::TransferService.new(current_user, old_group, project).execute + # clear project cached events project.reset_events_cache diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index e4ae3dec8aa..5a81194a5f4 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -116,8 +116,10 @@ module SlashCommands desc 'Add label(s)' params '~label1 ~"label 2"' condition do + available_labels = LabelsFinder.new(current_user, project_id: project.id).execute + current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - project.labels.any? + available_labels.any? end command :label do |labels_param| label_ids = find_label_ids(labels_param) @@ -248,7 +250,7 @@ module SlashCommands def find_label_ids(labels_param) label_ids_by_reference = extract_references(labels_param, :label).map(&:id) - labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id) + labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) label_ids_by_reference | labels_ids_by_name end diff --git a/app/views/groups/labels/destroy.js.haml b/app/views/groups/labels/destroy.js.haml new file mode 100644 index 00000000000..3dfbfc77c0d --- /dev/null +++ b/app/views/groups/labels/destroy.js.haml @@ -0,0 +1,2 @@ +- if @group.labels.empty? + $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml new file mode 100644 index 00000000000..836981fc6fd --- /dev/null +++ b/app/views/groups/labels/edit.html.haml @@ -0,0 +1,7 @@ +- page_title 'Edit', @label.name, 'Labels' + +%h3.page-title + Edit Label +%hr + += render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml new file mode 100644 index 00000000000..70783a63409 --- /dev/null +++ b/app/views/groups/labels/index.html.haml @@ -0,0 +1,20 @@ +- page_title 'Labels' + +.top-area.adjust + .nav-text + Labels can be applied to issues and merge requests. Group labels are available for any project within the group. + + .nav-controls + - if can?(current_user, :admin_label, @group) + = link_to new_group_label_path(@group), class: "btn btn-new" do + New label + +.labels + .other-labels + - if @labels.present? + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', collection: @labels, as: :label + = paginate @labels, theme: 'gitlab' + - else + .nothing-here-block + No labels created yet. diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml new file mode 100644 index 00000000000..2be87460b1d --- /dev/null +++ b/app/views/groups/labels/new.html.haml @@ -0,0 +1,8 @@ +- page_title 'New Label' +- header_title group_title(@group, 'Labels', group_labels_path(@group)) + +%h3.page-title + New Label +%hr + += render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 27ac1760166..f7edb47b666 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -13,6 +13,10 @@ = link_to activity_group_path(@group), title: 'Activity' do %span Activity + = nav_link(controller: [:group, :labels]) do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels = nav_link(controller: [:group, :milestones]) do = link_to group_milestones_path(@group), title: 'Milestones' do %span diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 8b1a8a8a2d9..c80210d6ff4 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -50,7 +50,7 @@ - if issue.labels.any?   - issue.labels.each do |label| - = link_to_label(label, project: issue.project) + = link_to_label(label, subject: issue.project) - if issue.tasks?   %span.task-status diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml deleted file mode 100644 index 71f7f354d72..00000000000 --- a/app/views/projects/labels/_label.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- label_css_id = dom_id(label) -%li{id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown - %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right - %ul - %li - = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'merge request' - %li - = link_to_label(label) do - = pluralize label.open_issues_count(current_user), 'open issue' - - if current_user - %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span= label_subscription_toggle_button_text(label) - - if can? current_user, :admin_label, @project - %li - = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label) - %li - = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} - - .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do - = pluralize label.open_merge_requests_count, 'merge request' - = link_to_label(label, css_class: 'btn btn-transparent btn-action') do - = pluralize label.open_issues_count(current_user), 'open issue' - - - if current_user - .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span.sr-only= label_subscription_toggle_button_text(label) - = icon('eye', class: 'label-subscribe-button-icon') - = icon('spinner spin', class: 'label-subscribe-button-loading') - - - if can? current_user, :admin_label, @project - = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = icon('pencil-square-o') - = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do - %span.sr-only Delete - = icon('trash-o') - - - if current_user - :javascript - new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml index d59563b122a..8d09e2bda11 100644 --- a/app/views/projects/labels/destroy.js.haml +++ b/app/views/projects/labels/destroy.js.haml @@ -1,2 +1,2 @@ -- if @project.labels.size == 0 +- if @labels.empty? $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 52b187e7e58..a80a07b52e6 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -6,4 +6,4 @@ %h3.page-title Edit Label %hr - = render 'form' + = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index db66a0edbd8..f135bf6f6b4 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -16,21 +16,22 @@ .labels - if can?(current_user, :admin_label, @project) -# Only show it in the first page - - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') + - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? - = render @prioritized_labels + = render partial: 'shared/label', collection: @prioritized_labels, as: :label + .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render @labels + %ul.content-list.manage-labels-list.js-other-labels + - if @labels.present? + = render partial: 'shared/label', collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - - else + - if @labels.blank? .nothing-here-block - if can?(current_user, :admin_label, @project) Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index a1bb66cfb6c..f0d9be744d1 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -6,4 +6,4 @@ %h3.page-title New Label %hr - = render 'form' + = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 68fb7d5a414..12408068834 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -62,7 +62,7 @@ - if merge_request.labels.any?   - merge_request.labels.each do |label| - = link_to_label(label, project: merge_request.project, type: 'merge_request') + = link_to_label(label, subject: merge_request.project, type: :merge_request) - if merge_request.tasks?   %span.task-status diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml new file mode 100644 index 00000000000..40c8d2af226 --- /dev/null +++ b/app/views/shared/_label.html.haml @@ -0,0 +1,53 @@ +- label_css_id = dom_id(label) +- open_issues_count = label.open_issues_count(current_user, @project) +- open_merge_requests_count = label.open_merge_requests_count(current_user, @project) + +%li{id: label_css_id, data: { id: label.id } } + = render "shared/label_row", label: label + + .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown + %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } + Options + = icon('caret-down') + .dropdown-menu.dropdown-menu-align-right + %ul + %li + = link_to_label(label, subject: @project, type: :merge_request) do + = pluralize open_merge_requests_count, 'merge request' + %li + = link_to_label(label, subject: @project) do + = pluralize open_issues_count, 'open issue' + - if current_user + %li.label-subscription{ data: toggle_subscription_data(label) } + %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span= label_subscription_toggle_button_text(label) + - if can?(current_user, :admin_label, label) + %li + = link_to 'Edit', edit_label_path(label) + %li + = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'} + + .pull-right.hidden-xs.hidden-sm.hidden-md + = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = pluralize open_merge_requests_count, 'merge request' + = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do + = pluralize open_issues_count, 'open issue' + + - if current_user + .label-subscription.inline{ data: toggle_subscription_data(label) } + %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span.sr-only= label_subscription_toggle_button_text(label) + = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel)) + = icon('spinner spin', class: 'label-subscribe-button-loading') + + - if can?(current_user, :admin_label, label) + = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do + %span.sr-only Edit + = icon('pencil-square-o') + = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do + %span.sr-only Delete + = icon('trash-o') + + - if current_user && label.is_a?(ProjectLabel) + :javascript + new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 6f593e8dff9..d28f9421ecf 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -3,13 +3,16 @@ .draggable-handler = icon('bars') .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), - dom_id: dom_id(label) } } + dom_id: dom_id(label), type: label.type } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } = icon('star-o') %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } = icon('star') %span.label-name - = link_to_label(label, tooltip: false) + = link_to_label(label, subject: @project, tooltip: false) + - if defined?(@project) && @project.group.present? + %span.label-type + = label.model_name.human.titleize - if label.description %span.label-description = markdown_field(label, :description) diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index e324d0e5203..21b37a7c9ae 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,5 +1,5 @@ - labels.each do |label| %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } - = link_to_label(label, css_class: 'btn btn-transparent') + = link_to_label(label, subject: @project, css_class: 'btn btn-transparent') %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 31620297be0..8c2036a1cde 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -77,11 +77,10 @@ = hidden_field_tag :state_event, params[:state_event] .filter-item.inline = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - - - if !@labels.nil? - .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } - - if @labels.any? - = render "shared/labels_row", labels: @labels + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index a7944a60130..34c66a17303 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -95,7 +95,7 @@ .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - - has_labels = issuable.project.labels.any? + - has_labels = @labels && @labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ba9f0c27661..7363ead09ff 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -107,7 +107,7 @@ = dropdown_content do .js-due-date-calendar - - if issuable.project.labels.any? + - if @labels && @labels.any? - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } diff --git a/app/views/projects/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml similarity index 79% rename from app/views/projects/labels/_form.html.haml rename to app/views/shared/labels/_form.html.haml index 6ab6ae50389..647e05e5ff7 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| += form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| = form_errors(@label) .form-group @@ -30,4 +30,4 @@ = f.submit 'Save changes', class: 'btn btn-save js-save-button' - else = f.submit 'Create Label', class: 'btn btn-create js-save-button' - = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' + = link_to 'Cancel', back_path, class: 'btn btn-cancel' diff --git a/config/locales/en.yml b/config/locales/en.yml index cedb5e207bd..12a59be79f0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -5,6 +5,7 @@ en: hello: "Hello world" errors: messages: + label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one." wrong_size: "is the wrong size (should be %{file_size})" size_too_small: "is too small (should be at least %{file_size})" size_too_big: "is too big (should be at most %{file_size})" diff --git a/config/routes/group.rb b/config/routes/group.rb index 06b464d79c8..4838c9d91c6 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -28,5 +28,7 @@ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(? 1').each do |label| + label_title = quote_string(label['title']) + duplicated_ids = select_all("SELECT id FROM labels WHERE title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } + label_id = duplicated_ids.first + duplicated_ids.delete(label_id) + + execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})") + execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})") + end + + remove_index :labels, column: :project_id if index_exists?(:labels, :project_id) + remove_index :labels, column: :title if index_exists?(:labels, :title) + + add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true + end + + def down + remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true) + + add_concurrent_index :labels, :project_id + add_concurrent_index :labels, :title + end +end diff --git a/db/migrate/20161018024215_migrate_labels_priority.rb b/db/migrate/20161018024215_migrate_labels_priority.rb new file mode 100644 index 00000000000..22bec2382f4 --- /dev/null +++ b/db/migrate/20161018024215_migrate_labels_priority.rb @@ -0,0 +1,36 @@ +class MigrateLabelsPriority < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.' + + disable_ddl_transaction! + + def up + execute <<-EOF.strip_heredoc + INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at) + SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW() + FROM labels + WHERE labels.project_id IS NOT NULL + AND labels.priority IS NOT NULL; + EOF + end + + def down + if Gitlab::Database.mysql? + execute <<-EOF.strip_heredoc + UPDATE labels + INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id + SET labels.priority = label_priorities.priority; + EOF + else + execute <<-EOF.strip_heredoc + UPDATE labels + SET priority = label_priorities.priority + FROM label_priorities + WHERE labels.id = label_priorities.label_id + AND labels.project_id = label_priorities.project_id; + EOF + end + end +end diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb new file mode 100644 index 00000000000..b7416cca664 --- /dev/null +++ b/db/migrate/20161018024550_remove_priority_from_labels.rb @@ -0,0 +1,17 @@ +class RemovePriorityFromLabels < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'This migration removes an existing column' + + disable_ddl_transaction! + + def up + remove_column :labels, :priority, :integer, index: true + end + + def down + add_column :labels, :priority, :integer + add_concurrent_index :labels, :priority + end +end diff --git a/db/schema.rb b/db/schema.rb index 51ac0fbaeb5..ce2a7752625 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161012180455) do +ActiveRecord::Schema.define(version: 20161018024550) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -517,6 +517,17 @@ ActiveRecord::Schema.define(version: 20161012180455) do add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree + create_table "label_priorities", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "label_id", null: false + t.integer "priority", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree + add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree + create_table "labels", force: :cascade do |t| t.string "title" t.string "color" @@ -525,13 +536,13 @@ ActiveRecord::Schema.define(version: 20161012180455) do t.datetime "updated_at" t.boolean "template", default: false t.string "description" - t.integer "priority" t.text "description_html" + t.string "type" + t.integer "group_id" end - add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree - add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree - add_index "labels", ["title"], name: "index_labels_on_title", using: :btree + add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree + add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -1211,6 +1222,9 @@ ActiveRecord::Schema.define(version: 20161012180455) do add_foreign_key "boards", "projects" add_foreign_key "issue_metrics", "issues", on_delete: :cascade + add_foreign_key "label_priorities", "labels", on_delete: :cascade + add_foreign_key "label_priorities", "projects", on_delete: :cascade + add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "lists", "boards" add_foreign_key "lists", "labels" add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 65ed9fae4ec..dfc762fe1d3 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -22,7 +22,8 @@ with all their related data and be moved into a new GitLab instance. | GitLab version | Import/Export version | | -------- | -------- | -| 8.12.0 to current | 0.1.4 | +| 8.13.0 to current | 0.1.5 | +| 8.12.0 | 0.1.4 | | 8.10.3 | 0.1.3 | | 8.10.0 | 0.1.2 | | 8.9.5 | 0.1.1 | diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 2937d5d7ca8..f74a9b5df47 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I remove label \'bug\'' do - page.within "#label_#{bug_label.id}" do + page.within "#project_label_#{bug_label.id}" do first(:link, 'Delete').click end end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index b14dd4f6e83..4ac491edc1b 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -65,8 +65,8 @@ module API requires :label_id, type: Integer, desc: 'The ID of an existing label' end post '/lists' do - unless user_project.labels.exists?(params[:label_id]) - render_api_error!({ error: "Label not found!" }, 400) + unless available_labels.exists?(params[:label_id]) + render_api_error!({ error: 'Label not found!' }, 400) end authorize!(:admin_list, user_project) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 67473f300c9..45120898b76 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -71,6 +71,10 @@ module API @project ||= find_project(params[:id]) end + def available_labels + @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute + end + def find_project(id) project = Project.find_with_namespace(id) || Project.find_by(id: id) @@ -118,7 +122,7 @@ module API end def find_project_label(id) - label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) + label = available_labels.find_by_id(id) || available_labels.find_by_title(id) label || not_found!('Label') end @@ -197,16 +201,11 @@ module API def validate_label_params(params) errors = {} - if params[:labels].present? - params[:labels].split(',').each do |label_name| - label = user_project.labels.create_with( - color: Label::DEFAULT_COLOR).find_or_initialize_by( - title: label_name.strip) + params[:labels].to_s.split(',').each do |label_name| + label = available_labels.find_or_initialize_by(title: label_name.strip) + next if label.valid? - if label.invalid? - errors[label.title] = label.errors - end - end + errors[label.title] = label.errors end errors diff --git a/lib/api/labels.rb b/lib/api/labels.rb index c806829d69e..642e6345b9e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API # Example Request: # GET /projects/:id/labels get ':id/labels' do - present user_project.labels, with: Entities::Label, current_user: current_user + present available_labels, with: Entities::Label, current_user: current_user end # Creates a new label diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 2b685621da9..bf8504e1101 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -86,14 +86,11 @@ module API render_api_error!({ labels: errors }, 400) end + attrs[:labels] = params[:labels] if params[:labels] + merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute if merge_request.valid? - # Find or create labels and attach to issue - if params[:labels].present? - merge_request.add_labels_by_names(params[:labels].split(",")) - end - present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors @@ -195,15 +192,11 @@ module API render_api_error!({ labels: errors }, 400) end + attrs[:labels] = params[:labels] if params[:labels] + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) if merge_request.valid? - # Find or create labels and attach to issue - unless params[:labels].nil? - merge_request.remove_labels - merge_request.add_labels_by_names(params[:labels].split(",")) - end - present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8f262ef3d8d..c24831e68ee 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -9,7 +9,7 @@ module Banzai end def find_object(project, id) - project.labels.find(id) + find_labels(project).find(id) end def self.references_in(text, pattern = Label.reference_pattern) @@ -35,7 +35,11 @@ module Banzai return unless project label_params = label_params(label_id, label_name) - project.labels.find_by(label_params) + find_labels(project).find_by(label_params) + end + + def find_labels(project) + LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false) end # Parameters to pass to `Label.find_by` based on the given arguments @@ -60,13 +64,50 @@ module Banzai end def object_link_text(object, matches) - if context[:project] == object.project - LabelsHelper.render_colored_label(object) + if same_group?(object) && namespace_match?(matches) + render_same_project_label(object) + elsif same_project?(object) + render_same_project_label(object) else - LabelsHelper.render_colored_cross_project_label(object) + render_cross_project_label(object, matches) end end + def same_group?(object) + object.is_a?(GroupLabel) && object.group == project.group + end + + def namespace_match?(matches) + matches[:project].blank? || matches[:project] == project.path_with_namespace + end + + def same_project?(object) + object.is_a?(ProjectLabel) && object.project == project + end + + def user + context[:current_user] || context[:author] + end + + def project + context[:project] + end + + def render_same_project_label(object) + LabelsHelper.render_colored_label(object) + end + + def render_cross_project_label(object, matches) + source_project = + if matches[:project] + Project.find_with_namespace(matches[:project]) + else + object.project + end + + LabelsHelper.render_colored_cross_project_label(object, source_project) + end + def unescape_html_entities(text) CGI.unescapeHTML(text.to_s) end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 501d5a95547..65ee85ca5a9 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -74,8 +74,8 @@ module Gitlab end def create_label(name) - color = nice_label_color(name) - Label.create!(project_id: project.id, title: name, color: color) + params = { title: name, color: nice_label_color(name) } + ::Labels::FindOrCreateService.new(project.owner, project, params).execute end def user_info(person_id) @@ -122,25 +122,21 @@ module Gitlab author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id issue = Issue.create!( - project_id: project.id, - title: bug['sTitle'], - description: body, - author_id: author_id, - assignee_id: assignee_id, - state: bug['fOpen'] == 'true' ? 'opened' : 'closed' + iid: bug['ixBug'], + project_id: project.id, + title: bug['sTitle'], + description: body, + author_id: author_id, + assignee_id: assignee_id, + state: bug['fOpen'] == 'true' ? 'opened' : 'closed', + created_at: date, + updated_at: DateTime.parse(bug['dtLastUpdated']) ) - issue.add_labels_by_names(labels) - if issue.iid != bug['ixBug'] - issue.update_attribute(:iid, bug['ixBug']) - end + issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) - - issue.update_attribute(:created_at, date) - - last_update = DateTime.parse(bug['dtLastUpdated']) - issue.update_attribute(:updated_at, last_update) end end diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index 78d7a4f27cf..a7c596dced0 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -58,7 +58,7 @@ module Gitlab referable = find_referable(reference) return reference unless referable - cross_reference = referable.to_reference(target_project) + cross_reference = build_cross_reference(referable, target_project) return reference if reference == cross_reference new_text = before + cross_reference + after @@ -72,6 +72,14 @@ module Gitlab extractor.all.first end + def build_cross_reference(referable, target_project) + if referable.respond_to?(:project) + referable.to_reference(target_project) + else + referable.to_reference(@source_project, target_project) + end + end + def substitution_valid?(substituted) @original_html == markdown(substituted) end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 2cad7fca88e..942dfb3312b 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -14,9 +14,13 @@ module Gitlab end def create! - project.labels.find_or_create_by!(title: title) do |label| - label.color = color - end + params = attributes.except(:project) + service = ::Labels::FindOrCreateService.new(project.owner, project, params) + label = service.execute + + raise ActiveRecord::RecordInvalid.new(label) unless label.persisted? + + label end private diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 62da327931f..6a68e786b4f 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -92,19 +92,17 @@ module Gitlab end issue = Issue.create!( - project_id: project.id, - title: raw_issue["title"], - description: body, - author_id: project.creator_id, - assignee_id: assignee_id, - state: raw_issue["state"] == "closed" ? "closed" : "opened" + iid: raw_issue['id'], + project_id: project.id, + title: raw_issue['title'], + description: body, + author_id: project.creator_id, + assignee_id: assignee_id, + state: raw_issue['state'] == 'closed' ? 'closed' : 'opened' ) - issue.add_labels_by_names(labels) - - if issue.iid != raw_issue["id"] - issue.update_attribute(:iid, raw_issue["id"]) - end + issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) end @@ -236,8 +234,8 @@ module Gitlab end def create_label(name) - color = nice_label_color(name) - Label.create!(project_id: project.id, name: name, color: color) + params = { name: name, color: nice_label_color(name) } + ::Labels::FindOrCreateService.new(project.owner, project, params).execute end def format_content(raw_content) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 181e288a014..eb667a85b78 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,7 +3,7 @@ module Gitlab extend self # For every version update, the version history in import_export.md has to be kept up to date. - VERSION = '0.1.4' + VERSION = '0.1.5' FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb index b9e4042220a..f755a404693 100644 --- a/lib/gitlab/import_export/attribute_cleaner.rb +++ b/lib/gitlab/import_export/attribute_cleaner.rb @@ -1,7 +1,7 @@ module Gitlab module ImportExport class AttributeCleaner - ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] def self.clean!(relation_hash:) relation_hash.reject! do |key, _value| diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index bb9d1080330..e6ecd118609 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -1,6 +1,7 @@ # Model relationships to be included in the project import/export project_tree: - - :labels + - labels: + :priorities - milestones: - :events - issues: @@ -9,7 +10,8 @@ project_tree: - :author - :events - label_links: - - :label + - label: + :priorities - milestone: - :events - snippets: @@ -26,7 +28,8 @@ project_tree: - :merge_request_diff - :events - label_links: - - :label + - label: + :priorities - milestone: - :events - pipelines: @@ -71,6 +74,10 @@ excluded_attributes: - :awardable_id methods: + labels: + - :type + label: + - :type statuses: - :type services: diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb index 0cc10f40087..48c09dafcb6 100644 --- a/lib/gitlab/import_export/json_hash_builder.rb +++ b/lib/gitlab/import_export/json_hash_builder.rb @@ -65,11 +65,17 @@ module Gitlab # +value+ existing model to be included in the hash # +parsed_hash+ the original hash def parse_hash(value) + return nil if already_contains_methods?(value) + @attributes_finder.parse(value) do |hash| { include: hash_or_merge(value, hash) } end end + def already_contains_methods?(value) + value.is_a?(Hash) && value.values.detect { |val| val[:methods]} + end + # Adds new model configuration to an existing hash with key +current_key+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ # diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 5a109f24f9f..7cdba880a93 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -110,7 +110,7 @@ module Gitlab def create_relation(relation, relation_hash_list) relation_array = [relation_hash_list].flatten.map do |relation_hash| Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, - relation_hash: relation_hash, + relation_hash: parsed_relation_hash(relation_hash), members_mapper: members_mapper, user: @user, project_id: restored_project.id) @@ -118,6 +118,10 @@ module Gitlab relation_hash_list.is_a?(Array) ? relation_array : relation_array.first end + + def parsed_relation_hash(relation_hash) + relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id) + end end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 9300f789e1b..dc630e76411 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -9,7 +9,10 @@ module Gitlab builds: 'Ci::Build', hooks: 'ProjectHook', merge_access_levels: 'ProtectedBranch::MergeAccessLevel', - push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze + push_access_levels: 'ProtectedBranch::PushAccessLevel', + labels: :project_labels, + priorities: :label_priorities, + label: :project_label }.freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze @@ -19,9 +22,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze - - FINDER_ATTRIBUTES = %w[title project_id].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze def self.create(*args) new(*args).create @@ -56,6 +57,8 @@ module Gitlab update_user_references update_project_references + + handle_group_label if group_label? reset_ci_tokens if @relation_name == 'Ci::Trigger' @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diffs if @relation_name == :merge_request_diff @@ -123,6 +126,20 @@ module Gitlab @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id'] end + def group_label? + @relation_hash['type'] == 'GroupLabel' + end + + def handle_group_label + # If there's no group, move the label to a project label + if @relation_hash['group_id'] + @relation_hash['project_id'] = nil + @relation_name = :group_label + else + @relation_hash['type'] = 'ProjectLabel' + end + end + def reset_ci_tokens return unless Gitlab::ImportExport.reset_tokens? @@ -171,11 +188,9 @@ module Gitlab # Otherwise always create the record, skipping the extra SELECT clause. @existing_or_new_object ||= begin if EXISTING_OBJECT_CHECK.include?(@relation_name) - events = parsed_relation_hash.delete('events') + attribute_hash = attribute_hash_for(['events', 'priorities']) - unless events.blank? - existing_object.assign_attributes(events: events) - end + existing_object.assign_attributes(attribute_hash) if attribute_hash.any? existing_object else @@ -184,14 +199,22 @@ module Gitlab end end + def attribute_hash_for(attributes) + attributes.inject({}) do |hash, value| + hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value] + hash + end + end + def existing_object @existing_object ||= begin - finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES) + finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + finder_hash = parsed_relation_hash.slice(*finder_attributes) existing_object = relation_class.find_or_create_by(finder_hash) # Done in two steps, as MySQL behaves differently than PostgreSQL using # the +find_or_create_by+ method and does not return the ID the second time. - existing_object.update(parsed_relation_hash) + existing_object.update!(parsed_relation_hash) existing_object end end diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index 1bec6088292..01a2c19ab23 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -18,8 +18,8 @@ module Gitlab { title: "enhancement", color: green } ] - labels.each do |label| - project.labels.create(label) + labels.each do |params| + ::Labels::FindOrCreateService.new(project.owner, project).execute(params) end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 3492b6ffbbb..622ab154493 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -1,52 +1,73 @@ require 'spec_helper' describe Projects::LabelsController do - let(:project) { create(:project) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } let(:user) { create(:user) } before do project.team << [user, :master] + sign_in(user) end describe 'GET #index' do - def create_label(attributes) - create(:label, attributes.merge(project: project)) - end + let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') } + let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') } + let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') } + let!(:label_4) { create(:label, project: project, title: 'Label 4') } + let!(:label_5) { create(:label, project: project, title: 'Label 5') } - before do - 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") } - 5.times { |i| create_label(title: "label #{100 - i}") } + let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') } + let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') } + let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') } + let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') } - get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + before do + create(:label_priority, project: project, label: group_label_1, priority: 3) + create(:label_priority, project: project, label: group_label_2, priority: 1) end context '@prioritized_labels' do - let(:prioritized_labels) { assigns(:prioritized_labels) } + before do + list_labels + end + + it 'does not include labels without priority' do + list_labels - it 'contains only prioritized labels' do - expect(prioritized_labels).to all(have_attributes(priority: a_value > 0)) + expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5) end it 'is sorted by priority, then label title' do - priorities_and_titles = prioritized_labels.pluck(:priority, :title) - - expect(priorities_and_titles.sort).to eq(priorities_and_titles) + expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2] end end context '@labels' do - let(:labels) { assigns(:labels) } + it 'is sorted by label title' do + list_labels - it 'contains only unprioritized labels' do - expect(labels).to all(have_attributes(priority: nil)) + expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5] end - it 'is sorted by label title' do - titles = labels.pluck(:title) + it 'does not include labels with priority' do + list_labels + + expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2) + end + + it 'does not include group labels when project does not belong to a group' do + project.update(namespace: create(:namespace)) - expect(titles.sort).to eq(titles) + list_labels + + expect(assigns(:labels)).not_to include(group_label_3, group_label_4) end end + + def list_labels + get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + end end end diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb new file mode 100644 index 00000000000..f25939d2d3e --- /dev/null +++ b/spec/factories/label_priorities.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :label_priority do + project factory: :empty_project + label + sequence(:priority) + end +end diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index eb489099854..3e8822faf97 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -1,7 +1,23 @@ FactoryGirl.define do - factory :label do + factory :label, class: ProjectLabel do sequence(:title) { |n| "label#{n}" } color "#990000" project + + transient do + priority nil + end + + after(:create) do |label, evaluator| + if evaluator.priority + label.priorities.create(project: label.project, priority: evaluator.priority) + end + end + end + + factory :group_label, class: GroupLabel do + sequence(:title) { |n| "label#{n}" } + color "#990000" + group end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index c6a08d78b78..f780e01253c 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -68,5 +68,15 @@ FactoryGirl.define do factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:reopened] factory :merge_request_with_diffs, traits: [:with_diffs] + + factory :labeled_merge_request do + transient do + labels [] + end + + after(:create) do |merge_request, evaluator| + merge_request.update_attributes(labels: evaluator.labels) + end + end end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index d04bdea0fe4b177e54770a1c25f83fd841f5a4bc..bfe59bdb90e75cb869cab6aa86cb9dc81ac5e225 100644 GIT binary patch delta 128273 zcmdlrFL>QK&3gH64hEKRwg?91C%Z~Rrbb`4^YPa7Fz;wk+G@=bD7C~fVAkA{<|#@V ziXP>iK}xbgO&%VcD@r`N8yY4taV@%k*7EsJ+vA^q{A;UQfByUV?Z*?|oRxb$t9Y&H zddugg`4grn{anA_;Q%)$)8C(c4gWv#H68d@^Z&CeQ(Kcmf`m*x7c=AiX2%4BIW7$U z?%$W<>};QHeJs?W;fd0*#s}Z(BiVJ7h5qlqapC`VeNWBwaP?@WbFo6NA9ie4|6^Vw z!zRJl{MX=L0tW|M(|_ZC0xCjIN({g9bDqs#|9}4R|4R;pb~H@w-F;yG!{x3ACTM7I zH90Nl3_ zN6X_00wRKZN+Xab`P(`EzZDbQfA`V*{qFTw^0#;V zyDO%+;C`OqpWC`lb-P_(Va~Tv#|d4+?IqZ`ZPT$nou)&@aEddwmw1A_1!u0u0z( zS`r?(h{!Rr#yBe!C^50M3pz^FOUNd2E_y>!RF%&HVPOpSqO;m9Cg^}qQWxc&cg$t3k@IapUvo3Qolo) z^^Clmz?K35mZqk~1qmF}8X6Nm8mKU_IVNp5P|&Zi;DCh$Q_F$mf1#KEZz()^_2l83 zQI96A-oAP3a^VE_Hnz)3oGv;Ntj8ZK3Mk3(vCMX0Ihf#KBGccHFj3=yFh@)Cp?$Y` z4j#ODH=5~nSK*P!%S{^$bohE37j7(IXYm-!2LqY% zAJ@Nf<92suWNI?-&|&0ea(rlDBFL`Jq{86A=A85)LWZ%e(V3&(JmE2e&x~-+APN7b z*^`+Y#as?7ZfI7TaKP#Tb3uFnlTbI?4=%T)&H^5R1Och`UwuJ*65-Z+CNF=izwGt? z19|UQ!W&LSh{)A>h=|CQ{@`Y6WME-((Bx=1SpVn&i-tq`q4JZZGo}e8L_LVeIP%bR zG2>qbwWJ3XCKK2an3&j^H9C(uJd{x3=sEabUBsoEd2wQaNRvm4W1t6%6t7d0VMK$B zf(l<%#b*`)#y-aS`Mvc?2e(K_F!r%O;Syw1WO(AF{6K((js1-b&#^}n4%jwv7%+Wv zk67Tipgzz;$syZ-hrOV+q2+);g$7e&t79RH0VA)9ib&U?ruqg}HWn4O7WGAf2No(y z9I$mT){x+4Z4`8oD9Dr8kS-#~)clWK%&GhMFWaDfbtSN016qLie14?PHAICw!oqKBhR_=1H&y$p|2GQ)Nj76$)REQL)y{NBpWh!m(IPh2@!$yR~v9Y0H0bjPZ!~!*$G7fenHwLHw z^V3}~3MmBCn}{_ZV!YYGl-O9s*sR>x#T24o!PxA^U?tk-%sA<*{ep}r0iG3l2{uwK z&5YJ;d0f>Mw+=ILxhXbeaj-sWX%YOlN}-IUX)_C(Msr`q%K5JUk{W-XJL<1g|MA_v z@{h$Q;&n{!@BHDZqI)4@V*ra-*Wu(32@Q=b8Y~=768l-|leiBlITkE1SS)bif62$q zC*t2ct1k!9IqD$VBT9l#Bg{gEt-Ik-fIx%Fft8I*541Aza#YWlokANAC*aB zZY>8GAGRD?RNyeTo%uQQV*?2lMjsd9P6umufrdKn#>Bb@kJKmb*Kqvz?os;0{Tht> zi$6~Gh_Vo>XW1axbnwwf4GFI9Hep2<3nA7g_6~v?V*G1Zx>R}oGmG1I{&AiAVQ%+) zmrI8_GdM(A8B$~}NG;?LQEh*@nd&4C?FpH z0tZAON6mAv?e8JBHbhmZFkY~!e;Fuc< z|Lz}=D!Q)%{2U7$4kcVDa7a4XSbM-BzkRM~g`IokR5yS$!1}Isz;foHJ}+E6c+w!o$PgnxNR=(DwJl zQGcb(jRGtV?e@N$R3g2ICGzXy((uUJv%W#ce3v zSqI{8iC~h-=<-;=ThDZekEgYPZ#Ki10HZ}678-1f8u2Cu?SEg0*mwRpI`ap}W1Kq< zFi7$B9ZG84#Kd~Ty}>zy63xIdNjdBhmp@m!9s7&&JUn1(cVRX>l+=*r=JKdflUL(Hg$WZ2+rbS24o4jt z-?{#AT>3ZiiN2i;H0g7mxE~8L=I~pGf~`s(JA`6vIP%*6tA98?xg%c2^M5Aum*o@V zFKhmo5O_n*Y0< z_eK7WYx~cA%Fg;FZ}y=6){m|2F8?<){?OjF-{jw)!)%}8fB%^JJn{dA#$Vcc`&Ib< zB{KgNw`UMq(9rC_m|C{Nk@G-E1KYQz_J#ru4yGHzOkIuje^#&DnY`xCl66h0j*}Fk zMA(`f8JJn5`fNiEL^MnjNQh&ZWk0KdU6A3LJeP^Yw`!G!zdsctLOuN^P78DQbDZ1K z(!%p2(?8+6*M8~0kCb-GBtN(%*3ELi&tZDoW$k~rWm1BTJ}&sbNt0C$7j2Ky*VAH(sCr#?%fBbk}J9M8zt1kBfh-8wz+6ctB=;Z_cAIc<(3ylkcHE(k8wLK~*_(MxODpX99B$QL*RFp`*!DMV zs(A40U-v$x+}j={JsmJx%+tokwKDg_@-1n&&+dn+npXATTmh@is*s%v@XJ?(&^v(+xGF8oS4drtaZxn5Xk_AAZK zkhu}u_fuZ(wolhEy6%0zYwzwY|95zry^>tB!YjRK!kwIXKj-ykKG|B$Eq%Or$`Y^3 zJHkr+TJ}ERHT;+3c;m&a`u(L%x}p6KUDN#I-kIc!C$21H+`CM$c1qm;JhkO#Q&fE7 zjxDlhE8dsytiNQ_(bwyyUlD(A9=b((LdkoD$$u2q7v9w8+U%(-IDJ|E?H$k6{t4EY zHvH9@th~rS`#!7YtxvHTqAODQp6q;3{_}xS?*Fw57ry9Rd7^yEGJngn+@kvRU(}yh z?3uLV&pGdpArqXUWK5f8|6jLh-Yg-e>Cti@HtoOm=kk$iY-t&3V zw&q#y6)Rls_51OMyW%Gg>P+rVQr&veak`nYhDi4lTkm}nZ2GEWim%8r^-sQk|7y^# zize^?o>6Ozsz3fN?y2VL+sWy3)?MDE_3W$5_hny}My1ILU*9!}fA?8+ugDk5HMZpZEn{8>};nlH6YkPI;^WW*EN7(JTltywyn-L$s*zc-8xGkmv) zYoBU$UF%JEwIkOrdVW~Lws%ea$4AWPiof=Jt9X8`MCnYgM(bXUpa|Pv+txdOQ!;De zcRtOcDa_mDIxDEX)wtQ?HkZBb^$^{a(@P%jDZlaU{o{Rm3}&|_w(0F_HPZiT7TfVkehjg9oCVk@2%G$g9%kwW@-*bP8tcadmER|QlWTW6aHcM6drb;Xr`*>y`ldtTmi0@=Tr{3)aN6t8 zxy{tn@j-I)w8%cAekLRP?Y}y2atO$Dzq5{bq&xf8v&-+7E|pAmRgZddXZ!YL3M)k( zZ;4&TBT)bIwCi8_?N_ef)U>;%X=l>W{3?b}RI`D{UfchYZMI~V;{v`kXZ~VI#ES{^h+#^0II_`Sfx0S;n?#1qPcWq3)_?tMIZ~x@@SCo~= z*K>xK_x6@amv7biHO2m}X!H@B#POw$-$K_t_}`jt4(WXP`nL`fj|wEa|;p32v#e&bwT@sffcsPSu^)sbk0lbJTsq}!~3#&y?A|PN|aAp zhS!USzQ%tGFV8&S8*iqjb#of?be~gxo7$JvTDY_QyI%Fr{pVALQr^vaF;k@TU*&$< zudMdbW5cViI}z*cWj%{cd(X?v{JCGeolWQO`R>-tJI_!2dHHXzm0HG|`k1>(zQx7! z&dC2=e>JH5)aPWgC}-h~Ovmga>;#;Q>lt;n|NH)F#+d~_{}=vDP(J%fXXcgtVe|XD z8Nb=;toYOL%{0_Nbn42LzFgUL(z)RVQ#CzbvhhBSHmbk%KlxW+nnSU6ui`hS>!Ldg zg+j%r$22AK&6b#uac+sq+`T59+$sN7)UQq3Av{6)@-?r?KinRCohtt(<&}=^bsNbS z_kZ)%pW>bIcmDUeE}IShzvrI5Yx1O@6>sX}Y+W|bIaswq!({7;gI`YVHnek>)1CaZ z-1X>6PS!Zh{SWMp+>x?ccXe%HV9GykyQ;R7Ed{w}`qwS%j+0b66eGj^j?1UkMOgZb zxya#JLUX6;iih96_SEg2_y1d`PxV%;zWbYxfAYR8uF%Z-iTmDo#{W82yR7*)+i^8z zri3PU=K0Huj#P7fEqoHZSG4MKLAdRUs*5svC!fB2aedC4K;w5|yjHo_ z<}I9c-L254toiVqin=e$Q+^)(@kuYSrlT_dVTk$6sUOu^y&g1pA=TWX zy@DSVl{)p@Gk$MccsSiXw6%DBjmNgx`yak8y7r~+!9>2O?a!I*wyot^VYc-`hT8fs zEgqTf4iB_`r6s)9m$iNyE?GY($u#VO&uX6!pO}SAr?u~76yCJudfqN)$vu;jcfCkG zCDvaZ!PUj_F)#kaI<5ulA209{&Xhb`Q$KgVrqJ4lSv#z&K5fo#j6`?kOBox@`I@yf(>7-2bPNm2WTK ziAk-#^%9dAj!qI-yY?PeQcg6tm4(^#`Xf(&n$K_uyZ7y{&8e*xf3mfH|6=#vQfK$8 zmdE(z!QH`rHB#qAXD^ZeQgra1?AbZ?t55iLSWOdPU09mC(`e?l0^`)iH>EwBZ!Y3l z9%j(aJNb{Cekr%^e1o9m`cSW~ThXsnQq2$A_M}zZ{?k!^FEi6xB6a26yDf|RSb{yQ z?w88A&^e)P|+?>^I6ma6)M~&#_+po8u z4|CHGiJ36ZG;IZ6b4 z+xX<@rE{u(B5V@=TDD#+WPFqo24#!M*=l=NpDzx zY2Cak!xL%xZ21{?mc>0yu80zUCjXP~=!u-8C2zJaH#ogR+U#XtOHt0Vp7rx4TP{pd z)0@e9T{VBrs?ROV+mAlbJCqov@=35ZEokjHxEPuc7iqRLFr?Gko*@nGQYZZQdyW73{c*kiK z*4eVN?wF_xOq-UUafw}eif{bTuwk+*iwkKzq?#BneDuu(>%KaBxAFk?gRB5tc z+XuaO>2JgRz59uYx}xGdmd-#v+ocZPGcg=<*vM=jGiOBC(? zIz4{zqs;iEx6~%XAla?E&gDwiW*h$85K>ZpdqT71-}KkqlWJ!l(yl$kYLT_c%5U@A z8)~WTKpaUR|HPN@Cr;ogZ`C=1XVAYQ9oB;=8O~caghJ-`Y+25--9Ry%h=P zKPvZhonmO{sqMmc`k9^HF)d@BCVOXhxSmhg8s7P^Cb`gd`MXuUTVg9A~jS%I2rpvduHkxfd?}UYPQEJLgirbGCYF=iA+vFS5C|aAxxb z>*t~lRnnb4?Ocyr)9SSs7+mwZ_u`S2s#ijd;78cXIFReG5x0qCA^oT4Mt? z+Ux!MC85PME%$J(qyHK0PZFVK=@&N|$32X?^ssK)&oKEG)}yD)n}1s`o-%cv!qT1X z6=O)fv)JMh6JNi6oV&Px{xA3b-zlo;Uwjvx606iMo|5Zg`)&8z zSK>ai{J*T5)39Is^IP6u1!iZGoH^qs|6;e@&F@~j^zQ8Vc{|UmpGxjcY8C$S>PWNK zub&u>Mq~?>oOYKiPkD^eFF{B3FN6txxQ(2?m$n zZ2fdcY$Bg)VOKlv>bXw*Gt1Svq{IV@Z3{d9=mc!p{M+*I`weSce}ly%tqbp>ci){~80+QE`b$i;WU2I3 zA)7_&*Dg-nGc{}7)a$an+UxI63ORT-*ZZBy5!F+tKb1v2z5jgg#?^NXe_s0{mlE{p z^xE_%zo)4DbS&Li|6%iawjC0a?n%$myWccNdyBAU%iYLcn|k*=?!MkjT+Vl!%e?l_ zy!%`%OrUAs-P}0O#cr;A8cy8u=f2OiDR!xS{g%=HxHHSD{i3tj%3s*uJ|)z3XqPt^ z_jAL?eJ58QF~5CwPDi!i?dgxp;+m=-z3jateLu_V=iaVj`!nY)rB&aBJimHZ`o6v5 zR^PcdB(Ltdf5&I}njKfO>)%W+SrxZ7{qFCcn|yNwF4QOf+sr-9! z+F@P)m)BRd-0aU?apJyN#NM>h-JZH~OI@}ssri2Hg#Dt)f-e6Q&;7|OdKY?WH{;$a z?d>IhGpo+G9BEd(_*Bxz?B?1lXLhqGeNXZcT9+%es%7SFfsbLk!ptAPe16mKSpARZ zX)P-(-|6Y)`CA&X+yPv?l$zP(lWo$tMw(>wo5b90UN>QC3WQyaYg-QUY?YS~KX=Y}1A!N)a! zZ}}HD_VszS)m;{P0gAKdvX#E=I`aKULul;H`bR%*yErL6QV;4=I2N(e>dX7(J%ST* z&CVX5zdB>Czz2zkg?#?o*eZ3Ui|Z{n?|OG_(~awIAD7$RGJG99Rk`}0h4tSn`!WRX zTdD1SbgE31IQ43id&;{hKdd+JU%P=r!za~T_nP3(&w1x% zvl%bO&#;|dzum9$Vb=fY+Mlj;r0~t3?Ib5+zFPj;rKe9LBY(Yn`sD7yWh*Mw@5h#= zcPMUhxbfu6>la~1QWxwNb5XCJ_4upA&+~52>bR!02yGQU%Vqdl=SFv}dd=iXrf0XO zF0FcNl%^GxCcib-{`!M5+cWtWtCNmIx1DLeuD+)It=936;{4C*MR@kbRX#jx__^xW zsukv;PE+>0pL|VUrOBi#?t6os$BS!!lC7A_ES=jDIMq@Q&Ai5Iyxv(``Ad-gtjeEP ztRJgM&D*n8*jg@W=IYXzb$92h&i%V(m6N)XabCqkrdF}elheENigVViQWq`D*w7~W z=2~QvdxqZ)kI>TCT%Nx%GW8D)<2NTfHUF0RxX}IW-3jKET5rl;@b0U)^oZq!cw>Rv zZ?~r0Bi9$27njFh=g-$Va9<^~L-h@(pX^nC?z)&uTDy;|aG3k+x!?b*8N_w3AN5@dp0#CepoWcf0BT} zHwoXj+ZBF2*m{1=DVvbSX#&zW*{`UV-D+m5pCih(`-)o5FRjX%WwWK0-}O{E5)=2C zqc8tVZ;Y$#bY|Y--C^mq67QA@nX1is!ewOgWcTbBjtuk5GDB@Dwc~YM^s-;)<^@)q z-0}2v`?otE!!6fw&xm|q+B>H}C18JX!rFJ6x{D6Z)UZ3HFB8UT^)?~?Xj73hSHIbg z&YryKr|QoYy@_APuICSboq(fTis5AOJC@qeq=$|vf%-zrar&7YO= z>(FXhE;GSv{JiqBHt?s{S5K>U$XSu!q_O|=t?DaJ%X7{)o4pnJG4KUwy~4ubcI|z7K$*P9>v^rYOXmXg0ezyF1wu26H$tt)GlJw9($YGN+fcWkSh zT5m@ELP*%sBHCtcXLr1u#qDHpCH%VG4fmB3e0J8XG3{A1{ms7RdU+Yq^_ssGrrAGBh&S_5^=I3- z_))iCs`kfwf=L_if7-WqV}q&s>Wmj^k+$0o?KyhyQEj&OGV34zEZe4UjJ_pZ*QK;E zcShgm^+B=0H|Oa_AIi$PP|h;dNGR>&n!p&9$|Uur<$hEBcHK+)RHZh%_{#3Bc1jt= z71^!dcZAe-CQrJZEq=8=lWCK|qABiqFK$^M*3GNGb}XuJ-_{n+`+GLUP8H%eE%lFI z5`F2yI)~)E2FJv+J~i|AD_d8`zWo>IdE}|EFrV&f_t_J;=A>W$%=|1k@5sb21v*o` z?yS;z*`I2yzU0~=m!jYeA5JZcyEkdqnaS(Rh04}k+f=tr zkhy;~zRcUFchaJxQR_aNu4prfSb5UkLhae(Qz!cbg0FY)3|v~Rz52A>%i||GoG-Q% zpN*+1l({Qv$fVA**wrcj*__p)PlQZcKJ8MMe}Bqn568PzBCqmnZr?yqNm0uX6`AwT~hU^`RLuud1^&xzHY2r zUHZljs**{h{DKDx48x!rrcHDmf&o5-EzTI(L4 zx!zFLS)UiwwTUxCL$Oq%&17@4U;91YD-|W-Rad7day**-I#^qK%EyZ0?ag=d9>%5g z`uBb5k~Ftfdcd5Zp>KN_87yLYe1;NI1DVeg-XI*N)K4_wphlkaYuS23$@Ps7Lu5Mc|w986dKe~U_=8XM0%Ee7LHt>`${Tci4zsy2o z&RxoxW-9*!lD5x?eC5kpbwk$e<(rVI?E#tDOVi8$uJa5iE0_3mhATg4heKp6-}iYw z=O;eZ&A#TA$-7P8u(iI@(Ydj5&y>_Pe%=3)Pv`zz`J?@t-Sp4b)XtV2`6=~c=XQOe z-QP>jxVI+$KC$P;zqCr}r<22irkwh{$2U9sBY*mrlh#@LIw$m1b?F5-}`K>Ime3|s0PRBN@y_oRJy)(F6+r^OWZGGLssSzp3 zrVIH(HpR+*36HpMfAL3$<+hWbuD#8OaesV$bN~CEuQzV}sxjNKJ<8}pqOR8|l^fGG z7Dd}wUHfF4_a*Xm+sn`Hk3Ki~zFhEKoXhao;SghA`2)v$mI%2ptCr`rozLW$GyioA z3lFBB$)nI-lERarfcGop}d}UP(R|RorEyH_`nZ?@Z|vv(yeh&6=CL z;#JZ12b08>n@RIZ{&z0QeAV#z*-8JK+azDued?d1{i)6KSlCP5O($7WnoYl}+ntbF z=E3`FUj5pS%XyT)q%^0%~uwlpJ_N{rq<>V%x0ey2{46TjwNh?5*$mwQKFfrEeX!^6Xxx zJlA}Z#+lOnH>?`0X4bTMBt^_!Lb(AzR{nO5<<-OCL#tI{2htgiihj^p&3hX-fz zU*F>FrleA882bZ8Z`y*4uGR0{R zf9+EGyXdmCUe?r*`-%T*g*JH6WV&ndQfm7vVVIZU&z$FEv!viGdn zO(QGSCELO(+amXGIel*0+sXGc#OK!Xy^?uwy;Hwl_|VI3bDwg)-@5EWWo>1_w>I71 zUuJLU`}#BeE&sX56TLBWOG|W?mS?rxFHpAB(EfBPW5V=p#-=-kZkJqbOgbxdRMq3+ ziiO`Q)7}`JT-m&u-K@Gx*?P9oPva6J<=aQ?)-={l=VNNxYk6`d>$1tS-5uW+E>4xb zBbylP&dlXCyO)c-;MMs1c~huGR#pN{W-&lTSGF);Mnxl42HH^kl_xGKVF_2n!$MR*xna6d<@8(a_fB1N_#WC~8&)trHXS?s?ruSTX z)ub~G{jq*pEb)84)#)c4o9@WZXOQ0y5y~FLJMq0u~QIp&=jDG$;sbl@@$j6|2I=p+W zZ;LgX)GaZKaBSUw>el_6Zr^ei-1v9q_|{WdH2i z?|R;;di84?x4)LT{!(O0jrN(H4==4;w1UHw|MRr^)l&EC6nRo5sv1i-T(b){eC}nf za_Q7mWt|BNL<;PG`DFz5?~#{IEt4trI&kJn^QnZ#Gqm~>TR-!!HFb{2>Z?oKeR0#4 zx+kl%W;m4JE9_j(HSxPy)S;Q8{h6_uYd4%ZsCoBi{j&J^#fG6a4qftwLL1Mf%lQ0U zxayD5`@D18Sts#K_pEukRU!4gRF%^1Bm>6;li-l3&Rgr>Ka2BeVqKZ}qryc_ee;pc zJ~J-!%sp1Ib*-iP#Pk#I_hlFwb7uN4;eYe#Lb~JKj`OwercU%^<6K!f!??!N@Ym0{ zt20$);-7kn&wp7j-r^uQbxp8ar2cdNAHhk|o7Vb=mZ>Ga{?T`1ee3s4;;*9j{hV+# zN8;v+y#Aj><^N-yLie9vx^32`hc7j@rHKmd*f6D}`RtSXJYb$N4|=+rA^`QaA|W*OX`>fAp&H7jW5woQ^V)gD_<_*wtu$`REdlhjX| zv;W-kzFd9%RMgwPMVr@2tyE^$U2%j*A?VU0o`*X_=kN6?FZOwEo}6)I`P-b;zshaS z1?|6AzvaQqpHDIZ_N#xo_0!#dUGE{A`SRzliOXw*Tr(}0qEtDz+p6yy^Xs-bnxZFn zZ2Y3KXgfoqVsy@;4V#Z^2nDZq-(P<;$UUdWVpn`-l*g3n>)D2`MZWf1%pGQbp8K9% z-EorjhRbKZ+&MOFm8gx!q(J4p=JT6on9VHn+Ml{|-Q`lr!slxL*)J9;`m61hn=n^z z)n5DJPji{sLpMpP*}e3YbGH4JyXN1_*WRpRx3w=%p5?Wuxv{If|Fryc{rwA~FU(wj zratS6{q;TT6y5h0Y0leKrl#|8gSi!@s4?$A6?Im6*%|x-Sq-fp87Z687_w9+`d=)D)vwNH<9@% zBHO(l#9k?!sPpQWoH+lTKdFP-iZ1uuTJvy{j8MpxwQcfZ-*TE8%!{_^$p4n;5dmh1ECE5p-b-gXy$ zS&(D!MX+hYhuib+*2w&=GhFF0_u;29&y9-uZeBTir4tEGrlim+Z4NYdiOVPy=i67Z?9azc{w%I_V2HKPrEnx99z95jxBuF z%jlIoj9Dz@ZL+#)lArvS6<+I!zPGyG@%jE<OGPVyr$`x-Na`Y|b;tGSB7qmajD?Mni$?VCMR5l4EqsZs( zR`Wi~Y&*ZzzuDATji=0nah-0G%<;2Akr`V;&wt~MOH|@`dZ(wh;1)yKpPhx%>_bEO z>cx&M6PBCX`f%oMrHUvEo#k)2K5KRq=_HlSJ67;p=;!WfXLl`Fo#pl_`}Mz-rAHNC zrXK%uB{h97pL*cKHD)26-)H%4*?nun_M4C1RTIFlE*{=`HXU$%>PVGH8 zd8O9P?1OS??_Qg5Km6dCaolok5nIGbi@&UsmR3F6lF(ClD(BU$#rKw5zd0`#I(yO;cefkAIOt?7Yb3Mc_ zze?ols+HgQKJtD2mExM?2UpzSd7hN9MPk(^z?hYkv2too}8O-+Q?v z=V^kge@yG$scEt0?@rnkX1Q!G&VTpq!mHFXSC_R3=Y)nBCvorL5 zt;UlLQCCkixh;Mtm%os6=Dxx&hV}J7x{Ws)Ih$2CX0P9u*lHihwT+|Y@9rfVHQvj0 z{9M9wEI-Ql*BnN+K;hHg;&<397t3iFx~aQHewbf%MKh=(v$XibFVlX}?~8X@Y0ll! z`QXOoPBzX{KHoREURjhYqt3mBjd`l}rPg=ykInXRom$9iohiq4?r6)yqxxxPPg{T1 zb012Y(K2P@My5qmW@O%T(pq;^X*XBqOo#r3-ht}&67`dQ$XHxI_+RtgyUj{{eGAXK zM^r83;eN`b`{r5x-s{D2t!{N^KWiviZ8^W^bXouIMgRIb_qr@f`jFOg`8unR`)tNV z2h*drZv5pi*HP1(-%aED$u4CnFU_1*MCO-T3N_EURO&iV5>?}Dx6dh;h45|i7fCH`_M$(}G{-iu9qhOJ$0|4_o&gnrX1Vn^Loy!tbTdv@+O76*B`h26+d6z@Ba4r^SwK^d{cP(Hal#_ z^zxG8^+DT04nNlVwqT*^Hl9n@qN_LE+hKjX$*Eua`8vIsOQvnBy;R?2VQCf~pC~z7 zN#uU_mpZvI><+>BpBA?yk$5ADDai!`Bsa zmiI=-O`PbmZpOB9|5bINf0|dbid=9xdVZctuf>_9hHn?=-QiW~t&a|OOnW~y-M}z= z*RstPiN2}LS5BFI(_TePlh+2b!`ow9DTh$<-Okg_qv-FDl6(5*`^;@m2%8& zb#|M!jZ~52bnk~D2Ho1Pw@thER{Zw*JyV~}+1olLsyU@yW>^2#qt5=nZzprldcG~U zH@yA1Y_{e0-BYvGOD!*dIsZhW-6vxkU)rhXFO6RMZ?;X7)o<13vE6(A_4!5H%G$5E zUd>4Vw_w)7rIqy?v@{A_Om;uK5_ZJ2RR89(&mTUAo?8DWuD3I~ZLLFD*|*&M$v3KX zHuRQ=tx0j8t79H`Ml0&4R=v?x$<;sCRNSndS}^Bzs@wbG>v@wOEWGjI!yAJb#>ltH z^847o#;?1PcS3DN!oB6|>L(O_vRgrwZC-Zbi=CuVg|MXzqk3? z)bGFTdHh1`$(%~>vw(V#cQ40ld3?hsx6rRvE^xxYQRa~GfQ_J03Nf@{#(`rj{xZ~nH4|37W#+{*g%{N}%cOycY1&GmA2?~6Dy%P}@jdE>QB zA?6-2eOE$?GE7 zR__;F64Bej`)KmAC5yg0wY}2q_S50?_#1b0amK^7bB(V}*X}=-C$s;<85RG|`cHkS#UI*tof6tyzohm}x?pke z+a(=Iq1&MQ`}8B3^ox(U zr6y|?+o4}hLED}7tgB6Yo;U55-18jozv-KM&QE>#>6*Z~@9YcxL5%)#>v=D#*p@GwAwS!< z{`H#XSD()aUA%JU=VfOr{zL0jcDYBWl^oi#@R4@N&h`3VxhH<@o@;Isr77B1Q?Ijb zlgd@Gqb64$ed&zU{_$tm)b3j|%HKRysgteLvMq0QaTO7EoZghEU-|6q&!mk{b(mDo z_x`B=8Kr617j>yqeQVN%>j(Ac_MLS(q{6k^Z?CsZX3}4#&no+6YQmrWSjqM4SfKRw z!;A;CPT6L;v~|lw20U=PzVY5mTTSoFM#n$Bb&6@!Fkle5Ker*{mZaMBkhdJy`o(Pw zKJ~>!YUOX;w<%_w?Ef2)=c~92^*$7p3O;;1kEi+PlKM3LlesIePM6oX_xVh9efKHm$c7`^_SIiX*Zma`TXBX-a@BTS{e_k$;;WpLJnqQZFBKGwyklZU&}96S8XbMz99F+ z>Xv(Fr{8w_n5+A_pnl_W&ZD1oL+_R6|59B&*{3Rw#iir^#GHy6t&1<%gkLV5WjpyO z+hq51in@F5OuF|yZhz&xhg&M1%Uqi~^YWELdrw_5eyiF3$Z1x5>c)rOk5_KG*YjDR zw=MAYs|>qHe&bH+o|7xzhg2Quof*Dj ztwp)nHbb_n?ml-H`ska@d3sQ1$}@T4HRgE*8`XCn*=qIvWn1gP6TQ~?ZfuwP=Ss)2 zb(GcAy9B>qEByG+-36>KKX>Kam>I3P>%s#DZs+~Xy=Q}jJi8~deb&8wDnnRI`qQ4T z*RQtd9@*=5SgS7ku;T{%&JO)QLibLrkX^JrIQ)I^Z0QaDfYn&_dUK9|DotW)D{mzr~W*Y#_Wuc`{`bB|tMRug#l$nQr_Z=N*! zD4`uJ5$v_W=kY(S*?A9_v3YBz&a7?wSbToZqLW*fc4}K&cYQjMdv}(`SG^~hotHG? zk8-{{e!49zbY19!CjO8lrA(g6n@@}xpL))jCF(zYhLk>k>o3~?>DbR(tW&40*!1

rTe?<73*W9ka!W4dM}^*Qs~Jy%7%N*NqtY&3-fZ^v{kC^q79H|euW?Ux ziRY7ZSA^4Zl?8x>nwyyPLmw-!rM}yFn zDMF5Y+ute%$CRE8H|~G^CF`?rtaa;WJ>T3nVfQm`lx~{*{k(>PG0&EXN5olzmX%-B z;@+FpTR$UINnLQttSYsg0q&c-eM9{})ob)lkWfE8t(I}xuN`3*R7+P}n53QiBJ$d% z=`+>#P5WSU^3)^MPv3XtZnHMJcy-C;*M1uB*E2pp{9=>X)my5|nOUYKbx$iPnZ4q$ zMC{4R^HTZuveQjpyjJD5zaLcg-EgiCd*$brZqa9Nf9x$ODzGaL7ys>J^XclF6Pf0c zqFnys^_zktulTRGiP`#Zj>Eo!`+{Gcp3DAX$(yLP$MW*R0>uo&BM%Nwys~MRQoZ`;8QD3tw%=QsS1riOE&O%z@Ac^X zS)T04ch9visX2Mm>PEe*dBTCDErxS`&nUh3s_JR6ddk_%`pm`cQM*`+jZ>FBTdH-) zG-BP9f={Mwog4QEX6B}J87YNl{Pc{xe9_Q#+qweB$*iNZu(1ANuOG%sQrN94@rsijvu|_R*p=U`#DtAr5uGdG6&z&s4H;*q_Np_8> z7~d|HA* zl6OZ{^llFAntQzW>xayF+~2j%o)KPBb@Y4p{OK0At+V82SLw`pb?UQf_Pea~a?O*$ zH$4+hJ*l7RaaVuE=K{5AQF|ND>$KWs$y^sZW5KuEe5uzbx|}?(|8=vmpKC6w>5_|_mv+CKAK3S$ z`qA67B37om%1#GZJed1;YIe-Gsb6lHv%6(lY`k^#{qFdp$@M-53j6)j*G7cpWb=H; zb@zVjUs!AMz4F@$o7ZP9b}~j!O_{3RtyOt@`>fyHt8~gg*|qymoWW&h&A)SV!QI^# zAHDe9zT}u2|JtRs$yMF2e^vE%)I@#P7ZsbVReI{4r`mIgwcieY zJIwY-|HgFJ+oiwR=RdeRqhfvX=Br;r?}RU$Hp`rQ=ZY<-CKPR*=AR{f_lE_)M*5rR zQulX_j)$u+e^PNx2@}b0@w+hL%IS`kj-P(tovE~B-%+i#mjnF^_x=AaKbKFv-p66? z#)2wyyM`z0Uk7}U%d`#k4-G$e%GK~j`p4~zpRXn-9GH^PFBWh`J4viwkEMRKp}_ja zls83%Iv&1Ha(dH4yY?5H-f>+0=Z+m#>G7|ZE}8M-P~gHf1^mXA(l`B%c~02(j-hq` z6USxA%RKL2jyeD2H@{4{l}CBy{**fzFPZB-(n?;IC6xts-e--qo4NSB!w0`v5sd#E zlHWYvm$h5#>?(hb&t~~I`v~!W7{Fm$EON5QD-YmGEu1_OSV~-Q$d8~yJ}#X=|FPyJQr z1AX8BO7u9lzxyxSBkd(O?#7>aqni_a^XK0`)jNuNmR-6n6A)hi>&upkdg+~xf8#d1 zvh-XO{p-rj-szj%bY`3se~@}q`C>)BO}T~g_CF@&3Rk^O+vd%Vta~RCUuD0c|6t~m z^G{43+*|WiyTrXHmNodGbdve=BcZ+q%k^)uO^!4CyDh-%_-v=cjI$;@E^B)1SoHqr zu}+iVb>_TFbsTSHTSfP!f2wC$v|hxTquAn`Pkuu0v%DL3%8HaPJ0x`^zv_x>zxa69 z?(WZ_iOo4CPgYd-+04u;J08AYT|DA#nu_-ZuVcFUw-+_n$)9CTWA@SNKKJQTX8q>H zr`)UiW{Mjdhc0}lWVOY}aov)CKZS))9GE%xMZWxJg)e(~RT*a8dntL(BDeE)Jx@%J zUQOqm?dvbilkAu&m#ua`N+ti_-+kL9zcRCMbR77%|JR(yz4e0P-{$8_32WU_KK{zn z;9%>#7a4u8&%9v`mN8+fd;32y)4gQxn)&M8UD7d$wjRPvQ;v16Jf>0|wp{N(kFaHl zqGH>jfTX`Kk0m*U=lWf;-#8~C?R!V2azOpB<8CL}90HtH7~N~&OkDMn=VU;c?TxH0 zrwup0oyGCV&U)=?lLD~NIuaoj;edo zKVi3}|Du9g$DK1T7yf&;!FFkP@7?eJwtfp=%z48)gR!cmTQ9eH>%DrZhnGrC7jBZw z+}mNFU+=N%ceD0Gy+hX%58vRu7EH#|dx^3CRrjCb9{Pa{jVU)G|EyWE%4adx z>y+!i-@NP&I+-FXBeiu>fx5VY!k>VTi){AspZb)w$I5P5TJ$|5sa~nca%Rfzq=@Ui;o&K_cUzs%JTOHui6c-f zo89Byb+PAj(>A#Y3vp}ldQWOfH@8~4{iXba)VJ4zx()qFRLAGTgt_cTdtoOz0Q)+ zYWnNvUS}-K#Pg@ES>mk?a1C;bDFBku?85 zs6zeuI23xm9f94u%h#>xwl@E zBePQd>`?VCfq>BNYg1OsvoDsO&zIkPdBYX1cP|2Bo(E2ypEmVeU5JTka>~PwmHe-F zHn%;B+UJ^b<0Nmu>qlX?r-$1yn6K+S?{@DTU*sdDiH&ON6g_lLP54KD` zqFTB>`$_hXC6`N6*`F-S5-81>Dqvfx{AsNX)5)5-|F_77AGemwy(o5VTH+$Vwav-f zR}^R4oniWWGj)muS+Ip)AZ%@UG9q(e3|#oZOWsXr_D?G3x@GKMv+DQ!uk#Y?tN%#(y*pybKV>^(#)Q(^;xA!b*Hn)@`JiOi zFH~V^`%+Bb({B4LrwQR7cK9!|6kl-6dZF@_mf)GYHo2O*E^;~LztXMfjPaw4J%6n( zY3g5|xMsGAdfdZ@RjarbpPVc(CUIyuY+|1BD}aoo*>E%1iIdPm7hk|Nde9 z9WA8;$90x8s@TRUZJ+btE-dOgGhf z^5INkYI0IiYS=$rYj1NsbL*2D$=B{#Km6}jdw_XeOnYwe8y$<=zP}fj$h=hNRo!f& zzCBBpJAH@2*1zTZ=l$rMAGbE*`=93Pk3x!?jgv1OdFFnq=G>yf8=H?ETa()=w(xMb z>(jow29Hu!IP;s!JgEOs^#03}UwG_l9xVal)LH?}8ye8-D-|CYss}^M(yIR}K{VATQ$2i(N=2MDk?eX0!Bb_%idOfQ3N_g;5Pi3{! zntjI?X1KqUShhSdgMsJr6yXb3a(w61h8{|m*>ZaG?7(HqV)noId&1Q3j9Si}_x-nS zznvLAseX}1g378#$)6@0U4ASSX8QNM>DH>gcR{>;adS>zf?rm0!q-Du**hJK>!mhk%-g-kQYUAH?3(); zeaF;{?whLRY+uMIWf}E%w~w`a@pH^?Zpoqw3jwa73%qaNJHJ) z!7FTD->kT_&f1CFnohm=@^*EGzI)B1<}Y=P500B&@q2%E{)g2Yie}ufn~})6e3k0C zJ4-8cwQ}X3ZGP(cU6l7;J(I*W>zkEgx0pJt+q)*G9ecB{ZrY1CYPWBkcas);F!59I zqGbKLd)x0nndVn=ahlfpx$QHS#_cdWeZYI>Ov#XC66#j7ByTyc44C-L9B&afpPN;|Nm>H505T#01IdS(XC zTe}owM2+^{GUGlnbK+mVtU}Y+{Oom~Ur!A7dh_w7G|QaueY0mDc%?t{^5kpohf>dV zyf0en@b2!7Je$jI5e8e%1>U&Y&GBx{-M-rzQCFq%O0TIib1p1+?J%M7ql_y@{d{3# z0gnuyOKNcu8jJU@WB-3>dU|Ki=?QyYU7e~>pZ2v)(&N^dDSPH9?cJ$d{Vva|cF~GQ z3!mKH{6k#bX~~^y)?tc)p~w7%d=>ZoUiReMXWgJlTRzP6%h-3z@5K?#%&20$P_0$J zxjW~#UVkk5sMD8u?)}H!XPlPlt+?2`s(kYIh(32(&h>(dV< zUevk2uWU!*?=JolnNF4vj?ejpVawRemOYd3OAA}|)Kf~cyFPZE>|=TMpG8h#;SU%3 zi|zWHl3FIq7jWw4$)A;_H#JX6TAE66zBJzb=F6@*mYvNda>eVPz6kW6X*GrCmf_Xj znkBv|FRq^Zczcers)Y9O`IbBC#HEyWm3J+j(Zy;qS+75E^5+}Nvcy-vTh6r8rpb-X zXg~AGUcW7oK@DfyS$BBVAN+sYSkr3N#zIThT_0E8Z1t4-Y~On}`o5Bv{<-N_b4%5P zZKrH14-(&@lqK^2r#FkhubcO8=xS=FN;W;2do_#4CoT79vG~dRvnK!3Vm{8vb>xK8 zj)0%zy4sXTrqv8MBwn-gqPR-{hyaEp6*sP2I#zBq*Gv1KAoH(F&P5yQ`wo6N^HDO(cdPV^j<>NVSPso*K3>wX z@b~japA3%Qdv=1)$LCvEo2Hn-apsEYwLM4d#lB~3ukTu%9JtK5oSAu}zw?~;jK?MZ zEnl`_|FRSJB3su_ESlFh!?;3Ws@sa^wGSlz|Bpz%!$Ml82@#j zJoR5(?RCAZ-0^iU=5(^uFMVfY)*kKr?Yp&PMa3_-clX|uKl&i4c*kP3_5{f*ynK)T z|2blmpIa30EV<6!?3sb(iTjE7?>#UxNd3)T-W}U6uNfS>E>SqN>88QyrP^n|*x%Y( zJMF|W zt$$P1+`ik$GT89kXUm%osji`}d#Co!wRB0Xc`PE%b8lyqtdnL|>36f>xpjV8x(lO4 z2Y3yKViN&n8*ZFKqn zW+g+(oZa*9T5Q+RlJ<>#y>PEpfyCljFY32+bj5wj4{);Fz4P9#zv|aMmsC7c>Gksa z+bb5ObLP`Qqm@N}1saZBdcgPZ`<|yiFV8)A_q6qXCzq&iF{$kDo-Dqz#DGV$^v~AC z%9CR}?o|J|dTP}orgz)wkExz5`ySA@@AP$xdH;^AVNWfP>d2g3(0Ia_{mcBbA&uUS z|CTQ(wyA$vV_|8q^SD}!;u=qjPcoK2m{+tiHtlwkS>N#Q`s)A6{28e||IdW2ek~FC z;m-=2ozIs~-}?UA%nEBS1uFC{ znq#qfrq;`cXQSWQT>ZDjww{Hhby?B$p4eX-ywr2&_|NVr`yBbKNS*uD1JQR9iP>x4 zztK-#cV1X#cED@7Tl01GVyqaL_+lfAK5@$jt1P|oW`{&`siCVcJMWx-*0`Q zRe8bYIrqt1LQb}h4%$sp@-M6d!k*N#xbOU^_h{-*?wy&pe^34S;>_2ZS8tcrGhbLB zG^fb)$p6(9YmVC{O<|Xr^6>P@)5&LCP9Ihbi#~aR|6MvSdx^etaI%8I437^!J1)9? z{Hz!t$X#d7`pWR>%GAvock@z&=1%E}d8=|(X0N7u^xKJ7rmBVseR&~Uva{so*YEbv zF28VilWX!;G~xeR?*Cr{UTt6Zd-un(n`iFU9}Fs+J?9|DfuC_k^CI4?H`^SRHm&Py z#d{;?J#imW_-Z*3O-i&zgN0vY=~wC3nf`FMdAEzBt1r^@95arFf@nE03iA;W#G!sDIa- z-rZfzOEW*FY>!;1x@FQOqpuD1u6Ml;vNcc6Y<#FIKe?px{qch{gIDH!_@Cdntx>ru zj^*e2JjvuKv*LanU(7!L{NtG(&*z%QpIPf=`$~WJOG%%#1}bGAyEb!`D3zTlKYChp zy_)Ot=Wjzdr~Ex%6*jN3IH4hH*0w0WG`qCAv?{4ZUe`89ynTG-sSf)IwxiEDZqW7@)tCZaf zns~{6()qT)#y@cq+TH)p9!P%NwORX<@TDT-)Ad`DH;L76GRP`SKh|@pZtgF=Lmm75 zuP#WsRNS#LcJJ|9`_!|t#G-0=lA&|-)t@k8Se?wa|veZtrfg&rcm|lgSOZI z%}IM-&$SY4|G?@}AU@-i=e(OG7dp!x9pbtvsD7pV@*q^hM&c)fV8$n<$$JDakm-&QyCp7>nxg7Biv_l#EOrRAtro$*<^qDR2!*wY;` zFSkm5mA-rJ!`&a7et-0uruX2>{ZreXG5XXOzrNw!ce#OEU=7=fdau{_*%%&u`IqVN zq0f6tUE>u;=UpGvH^1_qYh?8CwEFd&j=wM4;?B-YIb^$I+plxn#!3(3o(t$ro497u z3JZ&*8D+hKm6L=&RxC@)`@*yDh56JD{=NU?6S{;vZoJ-LlT)c{-#_V&_k?Xn*h9{x z%AM*Bn8)6dd+^Gny)y*hPALT|~_ulKqB$Y<g}ot zHpO?6mM1#Rc;J*af5l>(Wnce!@OOrtbw3mN|Aq9^B~4dvz2Tc_lxlb&=hE*p8)liG zo|$_^Vw<~Vcz%$eyTqzfVXSYRb5}LK5ly`Hfo0*AXAA+KGSti&+LV?CEx9kgXvO;a zyn0*rcZ{2Bj=yJ3TwW-*s@Pj*Z%9 zm@oXZy#DUF5fgjU9M-~jo^4;x%)GXD!|}-_|6hfMTw}T3w2(tz?DIzDi9!?oCSK0W zJ3f2Hf`+aW*0-mh{!)DL!I~MXUr)M_9^$j>qV4>8@z6-tW+$aXXJaCY9@{^>`!eA6 z$Hz6_%l`}O?fCh<;cNHZ8Z-5!Pp+jbeR}yqUG47GCw;F@F5daYUsHJ5{pxLUO*{Wu z=SOe;+4y0hDd+aXA0B3xyvd_)=Wow_luv)-+r6eU)l0n@Yjof zd#|+mxtGlQwof2mT5@*onaAt2YiiTKn)^F&uhEL;+_wGfmr70cYZpHqN}N*J{#RzN zOZckr^E96S`ux{3tD0+j>jkYAr24Kt+%aFVZG!UE^Y_atKbZeJ+uL~Xd-ufs zv*X`<=boPM{rJU)#}Cime&npn%c}DoO^JWpo2Q@beLLxQ=6nAiioab8ufBTqdg=Wc zldWoI|J@aRSzyZ9&MCd(VtLx!m05+lW{lr zSiSGmuwvhTji07ZJr=e}?_J{FMoZ;g}*Nfj?3!{ zO5EU4kzVqm&eNvnH`CA04rM!h&#Rmde$@ATXXEUYY0D(^9KAji@SYaDu*UUVU1ZSR znpXxtuNb#$8A;YV)G-RZb&V3|oBMzN|yn`Syj1Cj>TMzYr_gn#a6xM{Hwi;_EwW z8YU_^@cpkS-p?XqeChJEqRDMu(L5K5zUO@sS?tAllXJ%LpV8;s>yL-=EDMp6Hk|x; z$&Wkpc~3u!eYE^%JoMV&Rx^C&-lx>)I}>7pJa+(a(a!y<(t`-O+l+~A9^D8 z<=C3(wrj7b+v+6gkcsK2lw->1Je_o|zuQmNzWD93XD*9` z{+(s{yTN?l?Jjd1c$V9XI9E&+xbqpa9V)vkF6SymD`h2o2XFH*m?a|_dbwk zy>a-YWL0dnt+K){t2U1OX=|J2%#1k8VHqXeemQ^VEz?PlcZPH3ziaAeUbZ`}MR?x< z^&-=WzkClXA9Li=YWFkGs;)MP`MvA@%I{^4+G_QD4kE8E#cqFp+G3W&g?`=*H9IPf zGPg=Nb8Pq^|HSN5tJb3M>m`ZomP!eBu`CLcgl7JJ+2Z~Dh06|&i`|Ep=UqMcEkEhQ z=7r*VZ#Pe`7j6{Ydu7jqsWN%SS61sC|IsOL&@B5SYSV`d(WHQ+D3()|&F^M^`@~n` z;xcJllkfU8-}*N0SDDU!>(x&F5t@EepyqGJH??J==Ec|RO(IWtZA+Us{YOv$x6NLs zv@Vm?n|@1qZIWp;xc8_f`pl07(-qQ7%@QNjOrx0nXFghxx=f$5_olbzx5BsUVKIwN?%`v72Z}zvbvK zioIr^1?>hf=5yAJZ^VRrPTb2T-@y4aB-??@!u7Bp0E9kCkb$A z9)H5KZry%M?&`8FfwzNae--l84bymjwNdBD9@pJ%YZvQ#2UdJL`=4q1@0!GtPrFkR z`IN68j4*w)N`U)C{&~OGT{Fdewh1bG+E1%bk6;IizZa-&SH<# z?TPTVHgtbpf9M#e7Sa#;6f#%7Io&Gjezs@1MBKK;baG>vUp z#L5S4Z=EvMdf4fz{3=Vh!f=!0;SOt~C7;%Bmtisd`Km!X^Quat|31bKx?9xQd`s1e zxVD~ob9~B)l&kV}&lG|$Itr=@NVvm0w(~&ckZ^`+%C?)?~%*k^p z+YP_wG)kMYSgQBOZC`gr``ll@MhpA2Z{TYS9ia{S)XhYu%g zopj1^rWup+E~5`m`R|AC_Okc<;8*_CH>%O9L~VVi?#?3lG(8@X`CmVNs4}a(UqA0& zvwz8}5NDyJORk4_OP=V-&iA`EPuxJz_Q`&~Mdv?Bh8}mhp}x$IB~oL)Q6`5-v|!L)fx`9A#F$6j7>ue)Zx3hT!wUbS*xHc1E-m&~jD*;SITciycz zel>G;I5eg8j{R&KMObUAQbkpsPi7&7Y42Gwyw?5|o=?m!tjR z?a9NQH*X)EWB*wHz$=9nvqfd&x6i$QwsEH4i}R14i%ZRm`?l@lz1gV-)j1!pPnddV zS>Uv~s=Y7jZ$Ewcu(bTlv{Y%0lkdHa{CX#7p8Hi0rGHxgt#er2iYVsWd{Q5dh4UQk z^l4^XATU*F-ioL#Zk;~&G`pAu4YYQyYlihWzl^f2G-x<6jT4k)vxOqvt(7n zJPo7md(;-DWL|yxJ@3)xD6aBrEuH~Z=Wp+5Tx597-RI)xy!)QJl)fEHv3%doaZw|Y6*7_wm&n()=-2KjxhS80iSKjL z=d}5%)k-#8Z~Gs`YqhL3`0o@fcm2Ak+$g(6&tHh+4(q09^QEhQ){80L ziQ0d;A}-)-afbAFXa2J(EK}~RWHGpy$IDsu>)hHNq1fcVUU_PKXU+2uuYJFZr#JJ@ z=6I=DlW#uw#XTuGc>bd=Ck2#a1$t}?=eJ#}xzqQAt=DDJM}L{)Q&yT(x>hdJWqolx zz4qmi%9Y3cIK^+go&TnC<34AXjN%}s@9w+mztnU}z4?>0Yvt-wUHhMHcU-HV@3>#& zv&O=^9(S8{C+`%yHI4mGNjS54qsW2-`SKl#@hjKWXfEzwX<>Ak{X8MeRh%)G-uphsyff+{XB)Uh1Qa(YcAjDwdGM& z)M1*)>KL+c+lH_`uO%)AUQ6p?zRD_Oo*f_;Df*bqL2{Z7N_1P$x`^%fJ<^|X04xl<(mYGzE} zUb?h~tlcV~V)z8;kHoZ?% zpY`C+PwqY&Hq>O>dFJcCPC6O!()^1^^7gY8M_725@P3qgzAeIN_G%WUR-fU~u~N!%vJ)G5E}rr0o>TvxGn54(PI zeo+ytVxU6)0@t1|r|kKiv}Vq^*mkg+~*?Y{hC#c_8+_X%7@3U(6C!ZcPn%#)u)#1@#Og3u# zyx2E1f8qTHkGhnfiP$(rbFa&@30(Mbo6a*q;T-83m$$j!ynkC#@b>R_YtIMl@I2$; zaR2YS)-sFL?RN||)vt2Zp4TDW|3+&0S;dA845r<}UQPxpUp4;pbqg<$6|+upk#jla zzKbW#WzF>PF9{X%7Te8a*`;vgzI)gl3z;>y-+ZT#Fq!lZt!2K%N%>@#lI zN_17%$%Rkp@98dHHZj&wefQ5uDYi#!S7vCOO*n4bo^*wK+1$VE;>l~`BWCU^xmVe9 zk>&1+D68s?E39>Hzkl$yU_#})h@cIhXYE~R6|-@wuJN}ym&+p9S=N3!yt!)iCBMf7 zawhYgC$mMKH)!cFJ+@?5dg-IJkLp)Umt3GK-s9I3miA=zzo)`mlysfuw4}e8>hhz* zQS+&eit+BZ-8TxXvMP?m`ZK;sclpDtwK?##_HxF>HQ8KKro`PSob07I@79Ow2lLc} z%XfI%HNHFiPp{UJ>v%*{?vKYQiaM@We)XJfE?IkWQf-Ug&@;$hyC z=3%f@Z1L(nSuwvRsqXvwC*=O8MTPe^`=^wsIoBI4^kL4v?tI>M(#iyjlA>$o@2|>L zah`N5F*BLH($IH-sbxvQCAV4LyKWvoyWnrh=T+fskG8(^{Jc=aG4KO#dpHru+YrLC}S~6)sm}mT@jzb7n%y$+VqzQ4hicUMZf7%M{D@ z>7V?3(x)5ub~GuA)xJFX`f{wVD%-_-TsO-1U!2p`y?s*uMselfsT*|;a@?Fy>Ak|w z;mn<9)pxXu|L-Y#GDB<&hpjrylYd$}r(V6j`tpPNqR`7Vy|Y?6IQDZ-=ZZ8etF_Vdl4cW+z7_whXUW>a z?^;1U-m7ahA8mbea`O)Fxy4?WmRSoYXkC0$U$?{R+kzZN&J`BN*!O#?Oq=Mm;C4b+ z&2;g`?$7MDS!~<7Gm?LuwQR82dBCtDDD@RjgoUC0``2Ija*pTLPF-@UX7$xV{dy0k zxh1FAvesxHnA0Zi+Afw+eB$zkMemkJsk0}yERc9QM?YUh{6Lk8?{Qo8DQ0hCHcUKi zCckz@)`SSNg?l*ExWo=>nCyJc>)!Tu-M^@Xg4gHx+)LQf`NNnmY3J#0=Vhi!Pt+F7 zdvvAY`pP%&@AQ|QtntjT`nsw?`^-a)2_;`W>bpW}R-0UVJ#(Sn!-BoC)tr{fwH6Ph zS-KvUtj~?p@%GVMvUNi4L+Lx5+kZZ|fA|p3o+o!-mur<5M!fNjIdpC9;;=OwUMF4V zdX?q$9oWaaYt^G5mHjcVPK(rd&5-zgc9+0}jE`(zUmu;ZnEU?EQaGc^EqZW3BA6tWLtN#^q%OCrH$2}RJd+l z`?WPirOZg_Hg~)9Ns(nI@>=*4cYMF1qR>>Ax$vjyjSA`eze-+O^)1%cC>pC@1yDa^|4%|&x4|+k5t6mu4ykn*fhUBf5nR2wshn7XB}=a zzInV<>;&h9*j?gwyWA4k>u+61Es&@**~d`1h$r%|QoSbRz#+DiMcy{6n)opVscrQ*1->~$<^Rp(kIJ^B&$?UvhFaPnTT4*}Q`tPxhZMKPlQ1Hp8*lt^R=Q zyN}z>jc_E3dS*MZ45O4Vb|vOJx&)bGb-25a-e zlCD{XTOZsjy0APd?5$t?=E*+!nv7@5=dajS|K0tGRs_@Zs@c1?zu9u4sQBuaO8p}d zRqnnkKE62XlXhs``x)FC+g-n^9JG4A!C)SitJ_N?uX%(YdvkQrcgF?YE*tKNo24(>uj~>q0 z`_sE8GpGKpj|fxm-f&&Q-_Bq~g1PF%e|PSh9gbZfpc?GXBC&OGLslkts(Ss`7pFBe z{I|6@xtnXpt9R>7QH{PK`+Q}s|AwP0?uPapb~dn*YRWl1%QD0|*Z<56MZ*8Q_)2HV)2Y0XJ*BB$5_4A zlW+Uk8+cXXqG2s(&ZO`}XC>3n7fUzVEAA4N6gtURJmHe>UPH|=l>=%eA10a<@_TCj zIoVxjbKjn=zV6qQ*^83u4fg~&2)IqUpq&ynYu1AmuO|nXJhGb3|75?O{EAcENA`ps zUDq`=>4L@N ze!(*W7i}(IvrK9~*7S0IW#@C>$y+bYwvq}+H~PN9?)xQ&V#iJDLH@lrcU5lR`d+{N zcWO4z=bT&4GHX_>ed(h#^Ro1elZJACWqMOCt>Rc*wt8MxpU0#P-|DOu-$CXi}!9=bN}rk-tgl;?fC_6F|Zm& z%&MBR#Xo#b+={Xr?lb!ztPf!n|FKWHds5r9WP5Y9kH;LneOh+y6f;Mmy0qeo*z|_4rr}C5?#*}7HMx0s*Iib-d|qN5M^X;|^Y;a-!(J=Y zbiQ?eAaeDmo!D~6iVyFC`K9zDT^H=zl~5n-k#8#4_vTJ#q|?HP89pz=;}Z7W);Q?M z68^N#$yrL-;?}|58i*8) zz5GedIdj6=-*wBmSiF3;&{j)!)=zQ!yNzC6|R z;Co7)xz-Y^|1#Pi75AT1Z0~jD`6Ov_o^@IngPr@eFa9&dE=bFK?ynW*-G2UDgH@P65I28LrZaQ-s0eAr`?VH=UtF$Um~F^yLD$XgO7assb6VDD}K(}zT|ou-_!MN zhr+(vpY5uMR{rjDRrt`ao`aL-O6`1CFP|rNsd(1=m6yMrXyW-%weQXD)enT?|1>>Q z>tSg+7+UChVokx5wDUT*m?oE5Wn2sI*x4$!IA(faMNUp6lTXtB70iKZr?m|abuUVo zwE4A$t*3chP_M7bgb;rHE8TSqZ+*Vv_cbGObt(IG=9Qfn2af9=c z)|mPgYo>}{xc2<#g;gv(-Ft#xKA-IrFt_1xZ03CLhYo3L*IQVG9@#=NHgHFq0ggze*-Z!sIHh)X+mWO3)6@RR63+!5_kbU^}Ihowux7^RIzAwh# zX8!NlRIYvdHyn~Hzkcud(~HOQ7VgW_Z@jQ<-yz)#Jr42p**)_f&0twq?6Ui``^o!1 zq#{?{ShIQ3WCibO$Cn?T`lxP`w|U6KrL~7{E>}x&$#aOfbyTwZ^z!?wD=WL!XjD)BOfP;HBss5M{xlr;)W_k3d4bvM> zo@u(&y}WDQ+kHD8luT2tzxmI%X)@R9BLYsmZYht08K+zc4*&CmjhR1y|KWsolj-@I z+{@DU3I6reu>2g+uFCq{dFP_gUBTIvEsCopU1u`hckipLHw-I~`+aw=s+!M2d8OkC z)eb-NKGe>BzfoLe-8u`d3&P^Jo_szyucn&!)?Cj!(PzJ^D}Vf-IZ5HUXmNev+LC!J z+#7x#@)Exso2whvVwh}mRalwlL+Lh_Q0{lVuJUS?ALG?8O}!;;tlK}g^mLi#r>K72 z+-AL>mrQe2TPBxWkJvY9rMh+7vxjrH$v-?W_f&qPRm@s;(KCR1{ndw#GhM#Dm{%V& zn|p8Yq8Ybm^y%9-AJ_dNdHhaK;5F&OXth<}zE4#uiJtFpkUynqb8S*WOF*z_o6E$@ zOBeoLJ5_q#2J`bT^EWU)F*B90>vGnXoOj7NI%(C3fIWiqWz7~%^?BxaS^BGI!ntXo zzl7=+eBH#+S$3`Ez2O1Nxso-n`dbfQ;ct9&qSeQyad%GiwF{D)Z}WQ{y7+8U!3z_2 z@o-V$u)MeWe_g+>XV~r#lAU@t&-qwIeuX4CNr(A!7@L7)LRAInxef;zNdN3O_h!8hZO~D zFL&F2zIEToa%zyxj?d}2X*-fKe4ZF&hjdBl+@EwoN%c?td(OiAM_ZoU%#mL08Sx`! z#_tg3#V@Y@Jl?W4rRGdoVY+yXoVbkIzEf7RdaEv_8Z5gl7q~}iDs#^>H?#R{KlW_v znbDtlzgE~n{lM{hx7`^b@*m|Q9*0r zNi)&x6FXzv^)DV>yYkxat@65$-}rS*lbXb-6PwGI$fPps>#u3a{7)u%>@!@K|C)88 z-p8740mpP2Lc)G6$qQU;*iaq)wf;%Mv57BtXL|08IXuB>>ZE%wvvMB!eLDQ%{G69_ zEq?}UTCJbfzkPojr|2JJJu8)xxtf;l%FSouMQ_Ah4A}4KtdnOtb@qbw-1+}FsxBr) z+Rl5mX;XoGZ$#R}d3u})ayD%(e~$Dy>d$11x%z(HWd^NlqM9uEPPwyEYQAV%>pWoBV&hCqndwt63_+{@c>T|EBS#MZwr4;+ddS|Y+t+-@T z-S2w#6XhmH%ofDd-&QXVdM}~)`QB=ot5agO-psssY16B-&kx7kJ-b~$yidMt<-$GL zt;;^0fAeU`FNOv4>Q^6Si+ONH_eFxW+?w{lnweMXe>m35O#8IJbXOIZ_p}{0spnH0 z7qZn=OaD8T9dI`0!u8-Mcb>`2wb_*a*w+7g?WTpdcW)>$ld5u85!vn@KRf@}v0%9i z>kHEt`u?@O9Q2+s?)m?9oo{waZAuT_KPTLbiK8HV*+18XFU%RLoG0?6X3w${3HFL_ zid%K(QN*oFj+5#?$gX&Pbi(PGa@MI&KLjnlvVHvryV{1|)eBe_MV#&QSrBQ@T>Cq$ zV(QUpOSWFQwoLD+?j48KbCmNP{>E5Osh2q}b0+$Y`1RPF%)JY>0(33omYmsYab&&i zmvxtXIEtOi7QO!TV29Cyvua$opES>x`nFl-5w~0FV(H!tv*w}$a_03Z>z7R_JJ70i zWTF3AmzIgoBF}B;Vcf~`$Ms-Y+M(mWBgFbG#9r6$NV%Y_E7kgwsl+j5rT(3anZ@=i z%v@F##2gj4G~cbYE4TX8eg87Hu?H8l`XfO`5ygb-j#aemt}UsR-q=z zoCEy_R~`%y*>`2TlBIU;{R>+TysvkXS(Mr5?ArTql84=`nRkl+E}s^W@icn%$!}{f z8R}}h-d23@Z{@8s2ie=6|K7%6b0B20`>Q!cw?96y?%O}to#nvZX<09?=bY9vw6Trd z`!PeWMs#+IospUHiCXT`naM_3*ln^$|H+dU~lP3-KFoXKya))XDT zcC-HTmZ@tmtrVAhx|Gv1#4$sVAvkWWM2~@PgOIk~y)22e&QFXlZf%loeY-CI{POjO z{k5-um%el8ft=dX`M;MW?W?`>Rn_Rxky!Ta6`>P6bDHD#d*pMjcxsxwG=1SgeRb~E zsHst*MhZKeCz&c#Nyryx8ZoPO-8pstS7=xN3ahp^_1*8Dw(gj3Hrey)OM77hncecr zVwvp8lU}nf-cbLunO~$rT_y3?YLCwfp7WQTmPb`PZyi(#ff6`w2v|#Yn-}G<;YtG&PC_C9j_RlT-4ko^jq)I zBpKU05eLC*&*b0L-#@YI#%BFRmjli<80nck_!=a2Z22x0-3yTqc5K@+`&CHy#{&7C zw?pq#-{YO9cR22IxwZ6n<(#=ISWnF|>{LEc=Xvl>wpWSAb@>^h*IU2D$JD9r^=+}g z#bRElsGiB%==Dlu?Q(st4)b8k+izd_pSdC7eD3#5`^^+x;3JJ;{8T~sk^^8BRN(S0xOX)|uEIkaUSi{zvJ z)~yxnzh`tiT)P!iQJ*d;X%ti}pi@w&ko~G9hB?jB#j=J$|qJVbC(TqaF#6?}2DZ&JLM?jpzE5gxb~9W4!Q%Hlcb*m3{JyBAHZJU8o5A<@+Q!HA z+kZ{6JQr56{qM}2va8+y`rP%!zxD3i9u_y>#i>Gb)v*XGH|Dgh-aG1+?zH@rIiqlY zjpZC4OFpyXh8idCUYn@>Uv=?;zkTH?v6`hb)(DzDe4)poxJKc5`~%k$L6$;qO8-}B zynHLN;%<5NI!v3tonNy~@2X8q zZtU5Al^aDbvRJ;dE!A<-D^g0Haw_^ApQd2S^>rtLjoO~~`nep4>^&=^Kez0``xS|^ z?KkN5Iy@C#Sie%vNRhMUyTqoe7v25x`@c>taDP*?`Em34zn*@5^=Br{)OPs3cGet@ zVrGRm_tyW>O^H{t?|NeBs1@aPKdkM}>SGI5?|g0?QCD|W!0f5-v?)E3Mo)x#u0Hs3 zjipH-U&y2BV6aW`F$RPEdM#ND#lbC?EvjV#*J>PTSmO|v_Rc;_;a=(g{W~g_YxvGs z5WP7Ebl$?Y+}qo7%WiJV-CcjrXje}8%bN6SXJ*!?9d})GXXf>d^J*s+Zn@3g7?c~S zczEf~>hy`BTRW!(1mCxqJ@NJKjr;F$$a3b0dd4pO)zT>B^LXZ>*$-@%^-r2q(1D-Mc6JVKzUO{w@f?s`xA@454~4ehTp|=QBI>*E zl@*;Ty8G(;g+jF-U+Z|p{|KG2l}k9HyNGkYi00Z?&sMGsIk+_b`NaByM$LzZ<$sDI~|{T3393IE!lA|2na>cZo^%>V4Qn`=zvL`7L%tJ?N+ z-m%HyyqRboEBvrbQedK&%;xncGB|a2Iu%zN+8k03I#NAnhu6O^B@Ib&kiIwqc zH#%M>C%Regy}G9KkyU`i9PX3U{&q&k20?#`7SQL$Va``e2zt)KSL&2j04%l1mf&Cl#=Bo@YhTG1q(q||xv z634IADh>)+wZBia#7K#l&DSZ6ir-@%ids#j;NAdIR5Bf{$Thb~m9G{xiaDMbEt=?NKoccj{%5nY& z_re3dc0H1HDq9`clyvwj$5U~6mtztNYj-BHh_78B-mz)#??b0s4wXHT(c2t%*Nj== zOX;MOJMP7~6|hID=Q2dCm2uzN{iEtymSyw?#;1HyQ|)mn<>~&PiOb}D=vFinM}TsZX?=hSU&ko#=XDiG!q?<9k|}dy*<-Ruy9iT zbVbXZnm59jCa(CEJo7mx--@UW-05vWy3WFrZp^v<#$vhO?>+rb6efI)1>V5p(A9+-~o^C07KN$M#%4u}H?)w7G55i7VEEeoNoI{yeuYYe)GcJz*9X zCdQ54x#C}Bj5wG~e|_q{v^AC?$93A({eL1j_wCPqK3`^9HTTLR*Kdbd?A~j4z~$sKO=MN38ViHYh4t5e=B(}^r{M5{;~dq?;VHT z#Wm$2GptUZGhLbd*|{{M?8~`JWz(ngBx-kN#MQHzXf&G~oDqCJ^;dia#~gc|re&s$ zNe8`SKV0O`XOU!>ec)cO+u_qsFG`_f> z!S~hCZNdx+J)g@eDkt2S*zD^nvQhR`z3=Jkxknzh-F&{uZ$(az)@E<%8?|NI=Po$g zt{eHE$D*)2v}4+Jl~w0`RG!_Ksp*rVsjlH5bjZ!_#hlkhORhyN|L4e2wQEM;GM4}D z&twuT_FXvq!Bu_>iMA1 z`DzirPfvawWm0lW@11AARBXBZ>x;b1bCzAczJABD&IAAYw_iW}_^$E!`1R|rsr-Ia zuN80p%tCHy`t_M@v)9%iFIxMocWPS0f%6VK>OVBePMCH}ZElu~{Dw{T{#zJ!^+Xhl z_{^JBv&v>wpKRWx<+d;GeBX3+x2Ab}L4ME2qKBCgUgZaxJr`E%vnjAlF3*nLVCS^q z(NfmWyVhM=&3OKMzG+(9ud?L+C%<-biN512eeq-0+E~L$`x{dhCMLW753SGsaN?_P zUb^V_vcsxgezzy>f8OIPe=*Mapvd~Y`)VaD*Qd)f+zWD;woM~>rq%V{#C4PR%kJru zn{0e-&-OEp>8m^i>h|$1PU~rpuD`YH+_X8a^9+jrr=_{iXfOKq)?<0}{tv+~f0y!E zAM==A+Reha@6f8sUp9v%o&K;?D@Q6$uP>Qupr^3-%!=t+YjUssy4sNbhI7Gh)``!J zf@+Sf_$cWozu`*Rhs|Xoj~N_iTl}zco27Jf^HM7zd*uh;KbuAzVBf~MbGqW>(g*iu z?0WVjibHYs%rLb)&TIZ$&m(Hi+5I%W_^@+DRe(_4NSmK*UjYKt{89UJ-sVSb7Eb?n+n$hDS=@}lth+td|&?UlEZQ1lpUwHsHhZ` zJ~_AZv4Q<5bLDSYJ!;imrJc`}9{w;?=a3gW7;{xCxB9rr(F3cqoNTu25>aX0Q@}nq zC;X99_u90aJ;%k(_hmjvulniNJ8|8v`-Im2D$jG}%kFJM`s;Gno$!9^PK?_L`0JWyzX) z8@WvH@AEw$E}wX}%O&wN=be&*Y3fXoAFJd}h-5G6Eq zoYvR8rpMO`6GJK$>&uljD%`!<=JNkqqM~T0;?=bA`^{IjZvU0J4|@kje*1oSc{5+i zr#;>Vk6y@ss+;@!iiQ5g;M1r4{{O8=SikM;o(a6swFbWwHvCm-sd*K}HY3({!}r^D zou(h^{uRygI?Sg3s`OmjjS}52vu5$$)<0ZpdbX|SvN!kjeTQQz-|kPY7kS1M@YeI} z>g3>eQHNC{o9nOd5?$tT=EuC`NY1CW46+*R-)@C^|Be5pnsSy~K;M_^xRr(U*@ZIu zj-9Q|G7LLCM<;Rj0n4rYKOPmju2b9bsQTsaQu7Dv_Hw#8Y_#f*6S!2ouHNt3`d=+y zCkwON8{FL0G$rdyz}D>nJ05VG`()H#moiQIwNI~i{lefYhd#Vl-PmZ$xo_wFk~OaW z;Z?6}FRHm%nPly0@_ZrS9Y6W}f-aAKj;~2Psb;g@YcdDj+BoCQ6fr*5dyASjPSeQC zdYU(N^M#4cZ$4>1KXC4%QOdy`UcR^X{IYsz6I`}m@XEX2OtXT&HXmMn#A5af_K^RK zA7ks;-z^B)dhEcOc_z=-*X#AVhp<~3JWF|zSIA{%czLG&Ly6Og3;Y}o%=@(G-o6Ts zJL09<-c$9~-MFkA*sb*10d{5LEpD`4O7C~3l;ZoSt^sU znCqRE&a$Ix95mv8ExEPnYK?L4Z)WK;hR@GAlm)$(-I&9*>i^!{m_oCCCIxj<{%!72 z+4Er1&ZJ{uzn?8q%1v!?oob@D#Yk6UfAjaO85O1T&Y#|(C2K6XVEK!v_W8}V$C(z( zetR={>r$6s^Ur!~w$^K>_c}a2<36kG;EPRt(-==xKb<&zp5Ow(Vj;&hK3iAUOzrJ? zzC!!{sbi02*WZ8I60YCqm-m% z&sZV2M5>st?$lWhLEDV2*XC*Uu8FuMr2D4p&Xt|N@ArL{czSr6kIu@oH>U5rJe_}e zwX)ZrchR?8g6Gx$o6PD~{+=x{RK%|Bm1kt+XZ*I$m)6AFuOce)$B4g8%%A98=l; zzyDUR!1(IFL(?RkdW$3fzn1^oFZ6HyV&}i_g;(pnaqnIcK5tjo8s!Ggb^pDDJ9q15 zbnY)ax^P!tu0+0;=G~^*%Vm#VTUi#y`nb~mV(wC25tcP2pZsJtm@irE{IuS*uC77g zwe`OjA`|{zUGiefM)OS@6K>dV2?^Zu)vO_Q^W$$P{`70A6mCp^b?ie!efQlfuJTGn zUD8~aUR`clF72m!;*kCh)#)vYXYN0HUDGrrcOpyF!pf;3e0rD8&QmI4ncr)fG}o9b z=ziD4Nt@>|Rn~J%OpJ(6(ft-*9h|<<UVQMo?`hZf6KtJD_=3I}D_uLz zo@KctR&9yE$-C2d{lBYQWIA45#Le@h{h^u5sqd^AoZI~@gQn)a=ZesI`*BITPjpE6 zYW*b~NqaSA?|l4uWzzSWXAws&8}*-0JJUOn<vNvVxX0Kcw*S2e7(A@)!%T_&L_^F$FC-8NK(l!rf zk;?Tewg}zoi(6N3;aU)t8ZGa1P3rEFoju7Leq_|#s`tOjT9O;HR@(X0gcW8-k6C%{ zG20_qXw)X3>FB-U{fRlzUm5q!+PpO0fAaJ#0nE;eXY=(m3(R?Du~A1u+U?&Io6lT) z%l@xVdRa4pMJoHzyH$JBI{5CeU+?G6mFCQwer#7@*~T?5^XE2O+MaGaVs-1urqBr1 z>BcKkR(5Rj*`gLz&vVY7Kcp!r{%VAO+X-8*$-Sqif3E&uZc#n)S7Q6F@OgW0^)NSZ zuKeC5_IS?@+sa>U7Q+0`DrdTI$eqdJ)Ms0=XH#xSH%m;zyH|bTVbO{&kK|;j%U9R^ z6Z$gy<-$d`Rw?P-)2sF6+c@uPyIPu>T9pmVJ=JT*wYw&k~eo|C-=m&b3PR==WNR^yvJ~0*Gi+jNVa`z z{|Z6~QkU1(tKSK;aSz;)W4kZAy!u{> z-Q(8GjR{E)j@U1}$Q^v|%k_?2k(JSBw!gUM9;#&RUD}DqY-yJbX)-^w%oB_CI)6 zqCWZV{-?Wy){B|PxE_7-AaYAYaDLYfP3@U26K|9@95z_?_UXi|a?jtZc2=tguUF!F zDP=IR>7cEJsalSjYGiNe39Ec9bA}D6i7eNCYDX6xyqv8n7}T;l&&Y9#FxyGn3q6jn z6*ljkANb>2O`^1eX~AB{eP6`9_c*5R%=(b{YU7vs6K4}-Hm>dTd7u>>C4a(p&(iwf zu)c@8Jx?|6kDKz@d`bfIua2)@J*>l5rLVJQYbpOS?@IMshAM-tVkaMImFnr)v*n-H zW?3@#M$FukhgGFKvn`}=$g?j$c&s61rntn*8Qd4d*s_={VkPaE_o?trfBUXs(n59_ zIUTL-kJp+OB-Z~(em&Jb(xPzFP8FWH^_s<3I$k%}`hJ@GX5-qjXTP7`U(c)fgk?^& z3%g-!gL{=hfYa-Dm-8G~END-uPb%8P@pS!#Ll>^8o!~Ou-jf%9XT^&LUg9<@v+5oB zxa9o#O&Thn-p#nZ>rvT)`t#Ck*&8>-%k){iE?!so>e}P8nXZ@5)Zd7nbL8Otwh32X zNpIyj7QymM{|n<@F74ZErkwt~O3!Ce1zY{M`J5FK-$wq;GU##L{C)lTp4;0J61JUP zx^tS2u4;Kytl#Ge@u%nb%r5Q>s`GTY&Nl62hExczbx-D$wK4Cu@-VyoJ)6sKZ4#DR zkyHMqD%wi(kFEGM(XtKyPN-Vdul^Oj@~`abJpIL5j}-)3I%WK$_8aJn%?ef5y6?C; zBlTmRlj?!btGl#YEOUPvBpaN#_oBN48C&x?X3qRmf1=#UcY^#=&iRp*EyEG zzC9zoUe4o_o5ZQvYCfF{`Aud<9#c$n?b+FZA3n z6{cJFo;mtP!*|+6o<*NJB1(FbxsRNgvo<>T zyY$(2mPU1jvmRJAS$QO%8zn6)TpYuwr^ zjZFr--R3uC)|WRTH2O`yj9w3+Hz;vo!-U?u|9WpF;0GT;r4_E z$=lyM%Z?h%rURv-7NOyd^D{p2|m87I$zoBa3s$kYFj#?rq=dI6O5Z)Yc)$a4w zE2@PXJ=D|pOI`5QRw`R$l3zbDbm|-q`=j=iF7JQN6%k)@JMYlVLY+?U3tAhd-p=IX z*Dr6YY2;UXE9J;|t3>Lqlgr$Pvf1jUuWx!z+x@)iMTken4Bkvfvy!~jvWM*7^*I-D z%RN#m7O%9voK@iNU1Z^#vczJ8-=g=`CZ`{?D*tZG*`{tSczRRyR68TN=!;tQymMHc zP0!p(Ie(<%;*|&A<|QmWAh)O8YhuodL@%c=zPcZ5L)aFD{_CjCy4DmP^?a($!(~2E zho&u#Q%(Emp1|n2_~l)%Z};n_ZJM2I<+1ujRqRrQwL*!9p2`F}-Q!+vxhT?ixxt$a zIz8VUeJW$Kzqc^Rtz#`%sk=?_^}l_`m;>w8x9+-XpA@F>>&AYCk5NHAX5Y36gedkN z`}|R;jXU|?)nJ8u^7$mLnb%-z26+0L!evZ;^n2Th;1 zC)@Lg#c`?X+lSs|Ox}KZLGoS|=V|8>V~mUvjx1q6+VJ7$gYZkiLCGbLTQ0x8dRm=v zUi+8d3qtE-9Tr+0IiR#))q^GGk#$o(ZQQ{llNQ)eF)K?oN9*Jpbz55%Gc5z#hkrx@ zO&7RbSReMGBX^bedj+Olyyd&&#driJ#yT(D?_T(8*18qXuAi>2KB3X+WGB^}=AN_I zMJ(9)McZ-3GwGf$Ogl;se|gpMBHy$BqHlak46FKdcc%5P>$!GVUk{0Vx6XCb>K)wM zl4e{eQI?ZvJKHUL@y=djIp>&f>w-AE6mRIOyfA(#d%=FCir(Z6>jlg*Uj1(tPn@@U zo9TP0)X9@VAI2&9t~_C<-Sp%7#5>2bB&rVvZ3;`?rCBmRc|z31*y2BZWr++Gudk)} zecJ0Xm1nlq;gC`<14+hu&+Du+xU38vJWI~REZp~L&M~ISw&|>iQFC|nakSK~Zo4j> zblp{!^+;p!t`~ye%W@Z=ou}3&-TXt}V*j$M?lFrmzOeSToH%LW%rD39ao_CS6LSVPh{jt99`0-vheRl&J8--3-(M%lYG!GKeuUBKbzQv-+A>6YToHv zd_RABm9$C0NGw!m;%O-lvjv8CY-UIwk-9NiyY0)iWfS;!eg3N=mHkNGC8tX-)JkJs zameb77q2;g{mh>JVQ*`k$or(+@cO`s#rMDe{=^}kr+l74#?vC=S9J{E!+$fHKb_oG zS;NT|U$VX+VeXtOrA9)lyXAdmc}Ux9)Sq#3nq{1yHFMswi-(sLrwX}-@9~ZJ$Z2Ff zDbkIaPA-DKbNAQJ3kWJ=h$i8w(0p6`paU;Y|rA829twZ zCx5ON`N((Y=&e7p@;7exJUOs3H916m$4pV~Yj$-yI#GM8@80`|yKhI}I}5h%aH)F>Se1l2LxV1* zTV3C5618dS??pWF1z*bh6wkPPwaT z2I5@h>homY6s8Nzsjp4Te?DnRw%_cvi@sTki!j;LCNSA#CW!oMp8h0&FKk+v7SsLz zLH?;e-Zxo3PiVa?XXNf!ELO2#_VX*+zKTiT&6~GRlA2tn-_`E%z%e0_IhT3r^F)Q7 zyJ?2@J4#O}%nl2X4SBx*c(~JB29drkU%2WWYK<2L|24l-^>feUFBSD+&vTZf#IG-G zOMB&ScrmWmuU+2g+5vOuzw*GLZ_-BS1oz7JYn8a>!jmc z>NYce8*RTByQ|aq^xx7GZp?E_rz-KiL!O|Ch;61 z4f@;OOvt<^v;X3D4X0T*Gj@3C$v;lM{wa0FwCd?Uinyl8ERTqi3HDvx>-ufRr0R(_ zf15ORluau>xhOKLlzHy`z@0C4lr`8cS}nvHY#V;g(b%1r|H6LjfGfEtKjxOrnZDhp=aoEMHAAe1q+PA&du>KWXpsY zK0`60xiv8A3FJAC+=-%P1UzvaT!Mr5|kgB>Cdow4!~A zOW1`B6motX*z)&A;>E<={e|6&=j4Z6T3}qAwC>)?{N>4FA04W;hY8XciSYQJ_pQR?V8B~gC%T4AN}v9@f$F;%;O5;j5`xj zl68JpPqfg&Pe#8(Pdrk2u~*n_((keh*K%yGy*#n^=i3`sWZADUzMAT;?R(xpwZ8rq z58tVPdV^iUla`D9OE*2DzvpYWipY$3N%kg6_x1w&gNeaIx=XpB! zxORVOYiV=o#A#m7L|8c1$sLKlp#9!+THNRVar2j*+fv2DV5t9w_^SbhFf&`;kzP4jF5 z4WGnB|G9K$;hwy_rrT@Je)H27xq2Zp?9_afxli`YnY85pUjLFuCaUV8>Z)ygJRMhN zO<5~?SNnYVf%@;?D{8b1ueUdBwkt?~zV-62H$U{9Y@a=k-Y-`CV(RDM_ebXFGd}OV z&UbzL=fck}f6A5g)aE;mcZxd{&0iSKyfEe1r|ZvrJ3cG-XOyKqUv{~Iv(jl9$HqCIFUaV+X&kBV ze!O_`gw+xEy5DzO{Jto(;*y*2{`&Ye#-I7VtbJTBHvh@PvBj+LVbHN>-&RdJB=fsUI{hPdwTfCgI2Bu;`;2hhMOL>rCyn? zAi;ik;_V9gW^Q%07|wl;U)v9+spXuDc~&DC#L|^AUrYJoiDfT)S2nnM8OL<5JXsb| ze{AiX8@Jqd^&4&Wn6_!p`ya1ckGkhuQ1WA=8Cr|9# zG&k8@F_Jq}sO0t{>qTtmtv;19ZMq=I=F{2paaCCTiBjX;1#&L!_jun-lyN(@!?Vbc zV{u0Fiuw~P>JP7SzF5b>f0OT7;I0zpQ(5aaDQ%hg{NVFN@y6$)ET{u7faM7VJtUufmZf@>1wY8mlNhR~n zHN~t;?B{DXYUS2@tH^NAdD^_^WkdgB%|GEYlq;7^yJ7v)#$zBtm*iTt7Q>EoofI7=Iq$?$=Y94M(*tLD<-Qa%$f7a?$G0BafQMw=kutQDf&B|DgVhB zD6lr3>zT~eSr;x3;Zu9=V6OGMChD6{i@a~i$?bKg>^b&pwH%w5k^bk%pUXxu z3Fks*EO%9mm~hyze|q=#voqtuzSut%sZ1#p-BDPtHGAiQjUVF-_y6#}@RoJUYxU3T zj@>`pzM1@S`El@ngyBzdedEmO@h|@` z_Pe)G+g>+nF@IZq?S*Pbr|Hve-hPY^_r&I!eQmp@?tIE8ecgxnGvPrG zPw0#A`*CXRTx|G7*R{Y-=x$F~(8a#%BHgW=H*U zZ9UWd52F9uOnvBBuCzmlefjqAIKlsSEA8G_*I$p>d5`1w|7~0C6PViCm;L%D{ph3T zpRNUG3Iy}*4OTwz^wPenW@~&}MIyz<<;Y{}p9~u(Px0jcdi(F=y)XW(JhWTevpu?H z_xU4j$q@!`LSnx;Jb2H{yP(4GWqomDJJS+BmCB>8wHrlKJ_YXQi0V#=DyclgbnZ@P zVcV3LbsP16Jop_tXQuL}>#LjA9@Mz_eAi*7C1MXAUMjH6$Xg`wAZJ!tXZ6X~amT-S zvT0VH<;do9x!bk<8sp!WcjH&}zVfY_k#_yo?KQ4pf`^}#Gb>A-nZT=cye{CNMVe<% zy}RfN!m>1F?TK5Trhhzlu=_**jmG&I&*#jOIz75$cwq0b?4oo zo3{_$wg3CK@n75KcTKC;RlffjzSsQyZU4s2HY!K=*E_Io5t9^i2uM&U;p$;mkLa#V zn`Tz0eVc27T=`ecK2PaCQFATNnq04nn%dR;Cvy3Vb+cdWShX%{?%S(-RZ3zu|5)zt zqVcrDcIsz2B?X=XZi4%h_ity{cJw)9Kcl3msIX{jNm<#}lC4|IDy%EYu2)^(y0vm| zWtDYJ&EA@_in{vvx|(=V?wb9k3FnzL-2#lS9b%D_l4IQ;9&^NDZ%M%q*F!!*7H;eh z4&FO7Ww&F3gTvjp4rx|e#Bb~I=2%J1>zDGM`&B|R6K*K;-Y_v*FStN!xhuo-W=4Z# zBPRPT7k3=Ho5Lw(&+63t{O|4ht)7V=70%i_g|bJPWz35CyTQ%I?4-qltIJw8v0pfP zz-Y--*V*Ugk|*@tJ2__=%jcVmwij)6P)k@}AJCb@w|b4dc#o$_amoSP^5Z95U$(5b ze7nC_de^(Dg2m5!d>NEK#y#abUn%FvCt}!i^n#)FyqfDTZW%qb6Igj@!OH-n$3N>A zOv=1EL6+%V0&nq3rmFgcgm;2}4dOa(spTK&ny{54E0Vjx%;VV1@Kt>s&nhJ>=A9B2 z*{M|K|MkX-gbBMMUH6->yO-&#`tD4wVL;Bm2~MT^_60HWPKx-T#q?FxaKWB%<72+} z_HFu-*7USPQ1R@kS)LzOTDeSYIgzU1njrH3VZF>`y;#N0^5_n0ExmbbZC1oe*Z*#H z^3#dC^-(wCz|Jpgi_B*3mb|xZqkz`}&6%NsTSIdvT(aI4DBSFDwse`ULJ;TT$)}&S zOEqiF=lxfCaO1Kt3kf5hiDKnf6Z=jtz3h4W$W?B4BTWhDgL^t2} z72Xxgi(S{%FJE7}Az-tX<}|lm@h9YDnY`oftTQ;-#l2U5Zy)#OdvRLj_e8DLda@UV z2eL;dP2iRat-EnGLmT;j8B zpVcVYswDh4#PZjiuxh^!zfV5)#&uUUZ6(&qSRCHG#3w^NxqgdZvf{7b(*t)zZWX9n zb!DQe(?q_=DGx-K2nU|lcKYCKn6xEjnreXGcWXPXnC_=G#(s?qqGv93-+HjFKJDOF zHPO^5e$6v>Z|_K}Z7r}7Ow`HU;H~(glkuqX)ib?KY+f2RH&nPYw5J?tIrV6US5;Hx zJQG%T4jmothc7aoTPxO^b7Z)(^BSv}yGkzI@%W!gvx0n&=iQHLks8Tn4ZkmB|0%F9 z@_8*i^BB{;lG$BGj3SM$hCc)=j;MLtX?&VrVDxOe?z(fL*{0jJ$re@AcZ4uZY+tboSPd_|Aga~*NH{0 zs%8b@EGKv9eEPXs$y@r`ccGXgNAB!b=5mnG$_T9gE)h{@t#+C7BI`XhZO@rSH5X1a zEL}K*@nwgv+3xAL_&2j#t`|OeXzA4BBK1@IzATFIn_ztH)5orix`>OP3U4h7WBi*j z>l8!SOY2g;M|G1fFznyRmwmTJ;cQBM(!;2DGl6){#jM#)yb|+&)*gyb+ITjs+|z2= z(V`a74MEHwGggcB*J~ zNO8Jj*BMki^*gKo{)U8>XW6SCg)7){U)v?OVd6)x<#`c5B=+w=++5oE(#?h~>agE^ zruv|alhS<|uYYgeu|m8n!Eh?WhK<^0>awa&V>jhkwSP=V;<+3vR}sOLape2^BVHai zUaV=Ku?Eu+A*b#>*hR6xGA?>_{WX6 z?tgb2Sz9p6=WXcKPukvsU)DX#4ApMsisd{e`?mVrN~7jQN=bG$wfXgV?Y^A1*6XGy zyj$_zP59qbukMo$Dt(fVPBb?j*#GI>LgyVFOnNG+tq;>u&foo`bg+JBrO2KJ?Wb!k z_grD$dYo~if@Q(0Wfd=fOP!ef&Be#^`Qf;ViWQ5uPED+KUSOkdFFtAZrX${dbF43T zN*voVqx&K2@yN_;w3ha7cA>rC~y!G6~)Ac1>;qP2KcT7JM&UG>sP^ESL?&XLx;ZHI@%zjZ+i^Mn_qyG&Eo6l}cfP`K>V!|JK)lxJV!m)@@Y(P^^8 z_FW&PHyN06NU*$lnZDs`;^vpDN*11-`_*gWyu)1clj=+h)s}otnA84us{z~9pJyIP z)dxl$c4nWiC*-Fia`ksu+QyuUdA+ss^P0+3KX{0xuHy_?6jbQBAmvu@&1tU>$!g9% zeZc%yb4D8H!50bX%K|tVrz9WBiC2A5Ahqs$@|C-5btXUAT59m__Yda-*PcZ7ZtyOD z`oX@oPa${)f5LT(5Xajl`+XKna!L*0-uCD8skF9whd*YEn;DkwDDGbOK-gu=v5O(M zqr_v&lVwADigKNbZrd9l?f&~YL1f2bHRTT?9UE_N`RzS->7Vom(;TLTGG*$eshdgi z+~t%^jH^zPp51BHbTqO2n!jawN;Laa@m~*4>(A86jWTdK6In3lPu$1T3KiQvPd6^v z?!%C^V8a(N{(8f(__C=hKVHvWdScg(h}oMj%m3-LR&dj3dyub{7iD%>kuiYdh=kLb z?zQ>aU#9)Zy2P018`E)>`SKyY_1kvNIAzf+RQUg&8hbHY@zOZI`D>4C#Zc^FCM>MEvexb z@`U-E@-pe^VzZx?O{#4Dp6qhyZA|HmPwRIrIeT|`+hdZ_eu>G2m%=9CTD|Sh+yGvwO=20+hF>}j4cl^aHq6qn6#b@=5bJW>r)kG# zx3EX|idoaAuliQ|{UO(*Dhwd!Eh{vKJAacI;8k+Vj0F`!B7!o_vP& z?a^7q#C3fiE?&zDrkbL6%)jpH3?5TRLm2ubqRn0$nbb6eF$$iG)sPETOg$tNt zrl&tzH~Bl;hT=O5?>RiasUzA>QL&%KQA6GSukt&TuWD%^)Hs( zco#iM*;GfRcxFw*v?o{Jt~vVD`1bB^Hmn7wl0}rNO^z~WejW=6=y-1FIk>&7|X-|iNjZkuF;tqiYXTCqF zzxDO@rjxUIEN0iQ|90tR@3gEXZbkxcWYy|}rxZz*C*{r37P@tzZ~F1eDKpOMs{Imb z<>{RFnNOkVz+Q#c?3i~)bY5v5bCoY=ONqIvuz1e(P)%BT4S%p{AWCWc+p$}jrSQ?kzGh5X7lZ_e73&Tl-F86IRht^TaQ)l)3)8*a~x z^6Xr{Yw?_OfjnL8v)SkOpO~F`xVK}{p2tbb>;CP2#BBF&(o2VXDzTTRUb^bK+sfvv zTJB2UhT|~}`$dn;`n_|-irjGMaMVv6cEu6$*BSmE9e*7}$ons*MbUEsFsX4Br@Pf?rI-S4{vXlF@#W%g@+ zauRvZx8dhr{`2;UPP_h_OSrxp^mlR0fh@h-r}-S@m1R?ytVRE>Ql7hg%kL+RNpVGTA}h0Jzs}ouaA%dBPP_=) zCZC5Lo6P!IyuEJ*wne|PFkIMR6>zHC{iEXo!8u~lOLtbsbZCFSvT2dOYOuQctDcU{ zd_AWFl%7u9s=ny#2V0HiU!@)PM>d_hJ?XDgznfV71Y3oToTU%)_KF7>$oxoW{%*7B zkiFE5r>;8LQ#V|Yd9T`d)aJbV5t#B7#;a-rNYD(U5b~&1t%^rO_{`W54D; zW-88K7S-82H>z*Sidk)T=l)6BKQBFAwov-+`MGvU@w0b4U4H$9bke(PDc^Y%-yRdm znmzkKNM6O2DZ#Gk8DQfa?kxUN6WW9nSSda-||nHIdEnrp%y&lT1(Ej71V(<|<}`6i$CPnV$0 z?XUjGed{p#ewMLbFO=izg&-#zgUeU_tIU_>Jz91-k1v^vbNa&QhJDLdIH+hZY~FKY zpSMxC`HgD6d`qPu*K!T5Ge2&Na(>Nvx%O_S4(BW3wZ2gu-Zn8^JLb1@7o=7{cC3$` zxo6L$_VW=pdIPr4_cEYynr50pIBEfeyy4CEI7*o^1ML*>78ZBdL{tq=nbMc7FK&#{Zj;VD9l!T@-^yQnz?N^h#JNpHo652#v`qiR&JpTuc}7=F#M02pYuBfjexXGk z4dR#O?e*pO-p%@y?jIO=$m@zrZS{t=?1HJ)lNX5uw16d~&yIMuZ9i?cmwPIl>iPGh`*F^n4UezocHLz7 zUQ~0aNFzDi=Zfwoh5fPFSCy8_EZWaxDDwQ~p}+v~QLMBHo+QMf<@7Wj8GrPVWi%Z@bX6Lhwed8autn}#GrOE{# zcfB)t5xn~EFNcYeC)$mfW4)tM z&+DbB`Giw@?B+x*UpV=}1JN^2^j;QctQRkGaAXKeNL?;mdFEu&Jhoi-+y2qz3y+tj z{yS1o|52&wlpdR-L``#(!D=sksaQ*{+pV@gAA3tpEAR_=nsdST=3BG&2ek=rSKOJt z^?COj#kEeK4$5|~4d>r3@U+x&$Di_xbzvP0+k9peoJY+sgU&6r zQ}sU=JY_2DyO%Sfrf_vk`Ox}oB}a%$TX(%Q|4;t%+fnsi>=#@=hyOS84xO z`?6hX=h@KNJP)eV*2&j3iOjH@dO<1IJ9H)!^Q|n^r6RW5PPYC%W3t~PZs)}V86JB! zZ`q)QP5oQ!Kezt{4|l$D zeDitTwZPQ^z5mZl7FlyzWHM{W`8|5Gjq@ypX2r40xV67FV_&>8x8;Yp`i)Uy=WZ<& zYi)fIY8SnwF43stWlpjW>%N~EsiS-`u!AUK9dz&)Ox(Z$71=ilGwwW)3p6o?mE;svqd;rxvxIm z>1(O@rW2d?t_qnvb%EdJl)W!gdwJ>uBtukf&&r(X;U$x0iT1H*gFyAY5|d&%w@1xLjejto*Fk(!8S859 z`e`v)a=R|3t_ zvR?ASH)lrMQ!4WBb0#`V%V!_Z+w$_#X1zY1E{}|9bG#?}#k88e5aYJJ8#(*+BX!S` z=;sGT^&=pf)bdBNj{HAZLar{7PDf3=@YDL(S8F5%>lmGvKk`YMl2(cNA1pgv@J@XNb)$27aR zGb;V7f2z!0p1RPacaJ~QBI_HMjEl>Di85P!oa4A@lOeO^`}|{o7d?Cw;q*`^;@+AMi3e3TbQE_QZZumz z$&T%<@~W4tlk>yl*gEpA-}u&BZ@f$^U(x>JWWATJCwHv~5_{a8_3->JtMv6TyKWwD za=h6;m)ZTh*`$@M+scxron-&Cs&k%l=cHE(T`$uc)1NxseboO!f6oua$1E+q=DpD` z`1eh(o3pX`(AEuGwTnFK|1rwnx!}NcnEz~8eZ*~@c}=FL?r+}o!C#1j;ePr3bjAY# zH>REVEwr(q)Uhu_@>xX7v`RLi_y>n-ET4w@E;jjZy5ee!&h1$YDLef(JlQiPoBtf^ zgx9jpD`Jb*e>q~c+N+4?tn2#)c0DqZ2LHtwBpjaa-+oUhr|1~ zznvgZ$hSQ;pW9Mqd$%g%BvDoddxr!2x64{H9%bA9YdQBrprfsxsSoYy_u@vNA|IJlns0VW40--!=mtd716f&lo3*vNDJ}yx6~e-EYP< zY}*TF2<~LxF3ZKV$&i)7+~LIj?dPqTma=W{x*@oOeY?R19z&V!wSNRxin22BJABx` zU7&_(8Qb>#o7nfWZ?ErST4h+@vp0Eyy5r^bf8}p)dn*e$rXkvTTkiWC+tSN&-tO7` zedqa3i;u@TD_%vH|Nr%B&8ah|UiH1mi`i>?^qlgc?DH2rrr-K;U-{)8o9{`n5&2b@ zXFs;uAePM8IlET2TH@#ASFT%B_zoCOl$o)u`ktfAGbLsvQ=bJJ>lXc6&Q%#z&lT-r zIsM#shJ~prG540;dzjGZ7iU&p7u})#NB{rY*J53(w-wIxnIV+n?^xoG`R{gd9Eptkgto_`Iml`W0?t8O37^Vbh zJlPVc#}dQVvu3*af&*9X*cXYL9^L)7`KVQBjL8apFY8}#KCYGYJ*CK95O^qBWm(yz zCG~E7_oFY)YfpF64vx-S&~RxJ(^e6_BNhMMg!n^${Z-HZ+r01akz7|uJ#$Q@ z({RV0YxPzIA`G%OuD<$^XK*@r>9U@nO>FG-VnT}@PAxnTb6?oWeQULJmgY*Ugy~gA zLPjzZ7`+aJ@6wafo0(<3dn5DG*|yU{q?fEoXkGPi+7wnzW%0!4cWf8Mp6*J%`15nG z5r_5LRe9?It@d}$-zAyv=2&>&V_T={$@gci=Iem1U)m@PNR$$77S4F(bXFOaraY-hR znBwK&{oDIurA5w4g>3WXJh^X09xH20?YApWCZ?#e|KOO~;Hp>q)UTdtk*gg0vb30$ zQjZ?8uy&rDFta=3i+?@OcJ2U;qmyhTzHMfh{PX8MCi5Kvx!dRVKT1j)8v>t`}f~>-x$H5yv3c9MMRIaF!9Bv?|0v{xGt2Kl~LQ4RsYdDzDZ_k zBZJ&>%S{J-p5N_0$<#afXkwM|0jc8tGKX!_o7qj;gZVaz%zo!2eD+|He(zR^?8~>C zB}!*Z`F7YNIGN2;a9^^{oO;oB+SzSB8?o8bH z{!vO^K()5_fK6B>yIt2PkhTIC_2@fKYmNgjCX-*^_wTWzm>hZ*!u9#{dex|?GUp* zX~=QBlP^GOsn<`J)6ds?XDq1UV6t-Uh??1Z*YoxotqC)0cs@O`-?Lxtb=v2DSLU3} zzO{_oZ%P}px0he%%^1zbMM7`q{$de!^o&kB?K%5v-_~c+_6qwVBTq$k?_9pOD5N#n z@ASgMo92J1x&8G;{i1Z|2^GIxzFR$$yYCjRdhPU+PqQVr_17O;A!a-4h{_XH_DbcK z%vFZIOb;6$&O8*!cjzhehn#il7cbX--yZVeUy9nVhSa__8Xq}bUX{!~tDU+~-#vMa z%nZd!d+BeFd(56~)Xo2-AekJ_sIJs~sQKqD_vp)AHFFb=OQy1(HfnSOh2kNI9a| zpE6r%>e^-RT=O-zOFYYGc=|80N{=b?@|6#A`lV9>MRvToDyzV4vU?3*YQwsNhZW|2 zzO1cr^JmxkCoi5cIc6El1v4yN^R_;+I$L`4gp$s6nTbZ@^!{Uucf34ayVbA)!tGga7II*sN*K;wI({8ST=Ux=*-cNn5-uK&CX=3G` zd(qK8R}~6gY473??U{1^8N(K919RmUs^(I&l{j8;Zj0nN_v=XX>->aI`qS@k6K)o< zs;mF=-LuvFCQn(czhL&l(ho+W=ih3D8~dMLtGjE}reA-qTby0+Oe*H-{Z(haiS6^- zHT{hMmqM@TgHz^H@>#!kY%2SGN&c?6pzP)|+n)Q@jYL;ttTJH1PgHv_Hw_Owe?%I2J{_VLwp>r?TPO889L8ywO75o z_~%-Yj{0kZK##E12X|}<<@YyRo3d2lhR0J=+2oA{Q%{t>Z0`x5ss5tP@^9M4=<}C7 zT4PKr1h?0Qd)sW`uD^5R=3%R>{4E!{TXtG9HxvZ&2L}pRRBf2MrR@2Y`%h)Nmv`k= zT1jez9uaWPSJ=4i!1MDrDmHpoq*d**{OR?Q$Fo1Uy>*4%ktMgTHa`~*UB0=}Ds_U> z%-^3Xx{O(#Y_~kj{N>dBiOwl4*_^R2^!Bdn(Otf!y;_s^=XW*9l~&sI`*r2+mhi7r zIvlewLiwwL{o!+ymj8MfdGO?+&+j(OTH$`C@|EKvU`fhQ1%t%yObnR7e_uEsc0iAQ?mXuy7lF7Nf zQRP-v-%daKw!)$n(HZCVpFAHC`nqaW{WPr`=i+S~^ydA!=pD(qber*|nMYp4>^~(v z_s`ZvOJZ}p9p{`bTzb`H!o{Xb32&ww_1<~fm^%6Hw0&Efq^F3?%;;HKHd#~9-QW(7 zn;D+yLDb@>r@UZ~FhviOHn=Y-Nv zU+|A8tiQhIp0#20)vlSxUv^tC6i#x=cb*qvZI&Wa|9oxanduI8B4=LTO<{We^bg=2KJl?GO@Ao%XWj)1k8C6_%RuFJ~EE>$h>azF<$N(uMaI zo!?Ayb<0o7h~2B;*6^?dKVTgvi4f? z>FM&C9+68oZh7)X^vs%P+KMt!5nSvX%^S}Y$V>@xZ2Wj;b<(pN{hIz(4GE6v+Vh$v zx>Y2;hDNGxw&ktKu6Ize&uFhT|1G`Szh>IE4^Hd19Q>E5y{<`3+TdsNvo{HUj%xo| zVQ}nzhI3C@?2$kFX6P?FeZTC?(xhXmrfby?C*5&jR8(r>zW?|1S&f5yCVkrieyo4C zb;;*lQ{Ol(>6Tg9`8$KJ*RSs6tr@j4_VFgOy54OQ^K@Hiq8a*Qx)saB`nmggk52d} z^6!;duY-xp@s}&Vv+L&M`QG^0@oZPtEXn8<+nR5$STu2!>eUnZt3Mq~I`San@~PNm zQPRwQyE)3<^IcnBslqim#ECC-m1p|GFFva4-b_8hUE#MbF0^sSxw-_sLn-2K${1%V zw597E{PoJJBi@Gja>mwh4{6baTL!lEx-%xuU8fe~cl}x~OY=lVF&oz|{(UYjaS>}z z-COABu%jWlVy4X8=i!DktoAb*`w9QK)g7e&)>cpJrc&3dGcnDFnldk~FFTQv<$Kra8|Z z)B0_f)a5ON1I3d|SY~dr+!yp`;@vfjbMCz~UuU{7ep&iQ^gY9oREL?QU(z@8;e~ zw{wnp#y9MG|KZx{cmCh9{Qhc-KAJn<_95HdtA)&I^)|H=j_v*IJ9W;h$aV7*G+(}* zz4c(!MFXbm))@&mE4*B`Wi6h&>}UMVY zEio?k-JTJ$Fd)EQ!b+uKt=$Dz83Wn-XV>oOS@+HTL-}r-r|m7lSAJUc%-r}cRrdSu zlvHky3*|BbYbtWpTvy4z^p%>ER~wd2?Z3+99?uV3uo+wuB_y6A;twr|)3F8!|CwEOR#rB|F8ErhL0 z>eCKyY!J7Y6=oD6@HRG|$7f;hyJI@$ zWzk19XG8<1sV!4vlsTVn#&;m{*BANa4LRxhm-58dS4=qicWG?qy0uBXQ86{Pl9Sql z#dNF6yIve;oST^Yz})B8BXeK7>7SH3KB|@eoX1|D{5L3f|LUe*-M+O?^F!mrHuZF< zEUGbnsFzZb*7wW!_v$Xb)9lvomS*iKx6s~pVEzuxCXQVe3q`A~%I?mUKYm7Jrjhov zf}0DIUNX97S+9TUF(doLmKo24-7e>uvgsd!!>{jJ zdMLW#wY%xWx_UnL%T6pzvo|fdAo6`i()pfu2D6jA1N{vO-S)UysutcVTl8YFHW@4BZ{wJo?)mG6aFS1D|KSuV~8jaLz zr(>j9UoG9#=OB2`H2q<_PyL%8w%ers2K@PH{zWh~@6Gm<6|q};k1gW&*X8tGbMjKu zqSL!KR<2q$W2?0N)ydmCepX+e7^YM`$;l{3H>s@Y>AH}IqB#jK=WlV_bNBMRr0vPJ z(o>ZpH_J>@2o3mh)adqxoCzsYB0el$`&8~!-2!pmc@M=;i9bD6$?~#fQ+S-Dcl~s$ zk1-pMy?Bi>7S@O$RQxET6yCGMI$A6XN z%zG#1-e}u-$MO=VM%_9K)?)KpLEMKqxnC_4bNaZm{B8WKf4xFtA8ssrmi6J_@l9*f z@>k8dvOVQg_3W>^kLbL<^XEgAMpf3rY1&!7vGp7M7hSgTiC!$1^^RF?zldx9{fDWR zOXMG={aSqVdh4RP57AdkV((sCX7)7pg-Pz|KMmXg+*h|MEvkEP?Elf<4a*nThD(Po zFZmU*`2VWxH$@8P74{`X9ZHtTu6C_{mUz_6xnRP@7caR3IgTC_T9zu8>1E~`aF{pW z{NSX=%}4Y;)*qSE&>$b$;@r4fb=D(&&8xSKo{!ja`QyfBqnG>4@U? zj0tX^x^FPvmVdo0M5LbxiX3$3*G%gQcDJu08AdG0g+dQa<;6lF%9HPH(7x#~MkKB%mh{kzt0 zGo$i@-`i58)~@^Z;TwBPw9VQ+^POC84@QcvVe@uKe|C69#r~;T5h-`2YNF2H;c;Ff zmUCgJcYSNZ<~2z@Q{)W0^WGo-{q%yS{g?Au@}Kkfyp6lRbk)7Q=lwHR_j0*;G6HQZ$DLNqWw3o4dEp?} zlh?`GJ6QDf`^CTiWvp89eAo7KHN19wv+m8@`ghrpvRRz_9=*!4Z`$bK+grH)S7QAU zzb&iTG=1W9pEqoZ-Bj-F;}+fbcEwXcrxT%%nqO~tqWS!J?Ls!c#2spj7j5L@%k9|B z;@AGX|HNv$z3OonkFogtzh`3Du|zn>cB$Sfg}6Mvs%RPM7fx4mJS6_zWla0nHPMPK zXMSw#eXsp9{HCg(S^kZ8S?K9R$$Q?1Uw3#4)yrH~ns@tPQ)}f(`zYbfY@&Qo-pc}8 z8~k({8l7J{|0r5E=ik+5^4HXOy!mxv!)9_?8k`Yq>rmHYd0rAaDPc*;zwP(#mqh1V z{oS+W<-FNyyN$ZHBzCO4X?aUt-D^5`c+mvgGY*?g_q?zaV^nD3`TF*h=J&KOg@xYb zH3hvpI9}CPb+aaZpJ?u#U-7Nim2a}<#p%xqI~fWML;5a!Q@*h#aK^XC7a8R`XC_>i zW%;ze=IaAaIlY*V)`yaI`2I8$5WM#|-{#TF^R_I1&+oo_?;+>LRqT%n>rJlmtqV_l zIi;^=BWa+*IzuByuLo7`A@(($IDBLo7s|I zy{=h3J=blM!tsL#7R~*syL`!&BPzQte13aZ;iiO4&$TrwEfqW-i_-6DhKCr-pX~$+9TTT&uvU-*$z2XQyb$IY%a<<-LM;lv?CNVL z2P+31yc1zO_tAoq#VS+0o^MZGDPv+3m$(VtF_9a$MYC!XZC4cn766^?4w^% zEj#Y5c~+C0;jYd6LqGQP+sACPmi-QKTdMLsQRv~Gmn=I)PVBM^Vp?Hsc;f7oN0Thp zho=-cu_^8SrOy2;WRsc7+kjn(mwP+=Kh6uxdMT3|{9JS$(^k&l69=Yl`I39KU-k+y%@ZUntH!^1w;+FBRQWob? zY57t%uWw^x($BN!wRYvHOMcAKz3ch0U&--E^7=UumzJM8o-4MlSH2>3>&g?e=56%3 zwxmPqNBwi&&0FLqmKOKy*nex@_UK!Of-+@qPmb|;>KN6{>kuHqt#Tmot8KT{1rKIb zDNX*2^NK19g_jyHyj!2i<+N&&l<&Q*XO`YQ=Mc;ia{k=2^zCIi(>~REt!;Uib3Jgz z)a|C*YR_-56|xbV0!k4b`fyO*6uHwuf;oop)ikE#eH2?c1zBY{R?X)*V{DBml6o6(H1D! zVdsD0oWuH*3D+98_5S`Ul+F88cZ1Af&35T94$J>NO6&f3n*Q4B8RV+BV1v;L^R@S@ z?Xv_z?&K}DslR(kc1rj{{WVK3i=S)le*dyTqtV}&OF4U?DMQZc6V=HtpV#}&mbc3Z zQeSeuAk6JbgQ4?*+8owBU-^g=o<09s?Q)N!< zXJ_YJVd1uamGkn1dswn}g#>FM`+djTt!lTJ=4~&(H0cs|X6%>FZxJR74PQ;WecJKM zahWS=de@!iF8#JbVV6#K@hOdxiFq7l^Z@fM^qvTzYcG~gq1vMvAq5>07a<4zVJ3MJi%IYI+Vmjtk zm&;Ebb zm=d>e-W-izJ%81ztpzhYwJL4g)_kpgeYsYam}%xL!jwK| za|vG)!(k8Bce_>GUI)F|GhMJM_vb=w{`5}vjsR<|)iuyj*j`v1zw&$Nqhqx;=T? z1F5*TPg%oH&VE*)B3)BIEpl4S zsY^1tO~(vsy%X;nXs-0r{nZj-@G@q(@v?#?kr(f*?Ed$sNo|eh+db*L)lD~YUvaBc z?bu^?|7XtSVC&DXXZ-o4S^t>sP;7aABwChuI6nzf7rLdLS!I zwC~!yKg_BxuYcZVojOzO{|dR-h4TCLWYbQ`iFYov;8Xb|^C?e4czN5ILj9K~&Kfl@ zTx;^haFM`KAHPtYd?U-Vr@79&mi={DB|1jZ&E`+~?m*$%uIz`GR3~m*me_Lt67L2l z{~ygu%#Zks8jD8EUOSaj>gJ&vw_Q%T2-HvIyYl_s!ITNhcbqUx?t3>YdE)w83dQrX zT_m=?WNXflH48tMV(0ZXssB+()7SndF}we4xVhqxMd*+BXP3Tm&^enI9=G3A>(_P; z$AjnZU9-*6E}q*m_mYf9<|~WTt{G3SmbON9-JW+y>62?xl$O&k{zG=!?yHvswmPKU zHo4qwRWFuTa46`(k}petA1-`z-g$k_v(#jH1-tDk5ifM=ZA>ILao-TU#Zx6+RLZI^ z@z3J%!xdjE`?_y7Eb{p7_sgT=MT*3il$iRdrwx{r9=eooW!{u&mRgiiB7XXKsZ7m7 zwV91`!hQWCuU-@sT>Aa!yW4KXrZc=-wntR`3|5>U#VT6#Hk%!mTYtU$M#-+yS3)&&DsP|h|IEC*aH(Vj$MIbK2|ZDz3!c<{ zs4-s5-qPf`=;W>NrbYEWpHF1JxRP#of6MwnhvR*W7exhlviD4gJ(S_9f9|`C-@ZK& zVap>8vu3!P_>sG$H$`Jhe}jN||zhQ`h?c`sXW$ohlj0CtkeYUC$Qff28BQ z{fb))cUMg;i+A2z@1%9E>h_Pttv@Pfd!O3++@;aNFl)-IiE4MMd1tFxt~?ehcc%2x zjNT2=CePP1+3-~gMea?$@UG79wROi4x5b_B3N|~jM=JN-y_x&L^Jq-^Dg9lie==WQ zY5B{drCMp_z3HL9ON{HpEx)mB%M9hOe^?UZ=6dSIqeUxacr323LI|}HuexzJ}eib?A!kNmiodl-6;2)(FI{gGxUFE z#2qP4{rJ>oDYwsA(~T9iU;1DANKN((RBI0R3$VS?_h8je$;DHSd#^HOe4--T%D#1N z>8kp<-Qs0A_or>OTpsRu(|y+ETIs3p%XGteoK-%2iiv;wSLwfWm3aQTx5=pscG(_q zi{G8I+h|!sYu0Aftq&`|ul}f#+~APn^zzunxaxiGo_}T(e&v62*8G)Tq~Dp)pEvgS z@8J}2Hr%AbmFh5|OmkkFW7WBZJr!2cz6Hl60()O7)aQr3bod?i_wVERx$@c{A1?i{ zA#q`UuH(LWvCWCsz90W`WRrJlK2xn$kcXMJ)U{lJYK9qS`l@@{wHPZVF|lrzUp1q0 z!Atf%jkh-R?0ejQVs8BV6B>8ji)Zd<*t#+K>6Oe+|DMaeTzBAr)lT8mQlkSZ&BC&F ze6wuVw_ER)DUn)IpSEw-*Lh!G)rP(c3^GqDHdoucj`vl#>+RyEJ(K!VMPoJ`{Gl_+ zPyX(l0AKQPfhuoaJEAeOCEN6$>XxX?{9*gYE5& z&(8VP#^Fa*Y8$t`a60;_)J{$DAivG;Yxzp+-q_vYwR({AFk=3mRc-Z`!}fPbI(uE^ z4)2nak-C=s%cV~G!oHPGCk|(-$<;gj>J}GFc``wh^|`$7E;HY*pFOJW*R0L=vvZXC z)K+*O%v*9Np}UuNRZo2WgABX>F^~6ktTXCic=vJ5`YR959$)nRZmh#(nYS9kxqTls zivKh(E;WxgQ*Kn^wEo1z6uB<7KCd;bSO1)rnM`rnePj9=Y;S5{FZ6fmANUO zqLU^q`(<}G?oGL2 ze^&GHE~Ss3{>{sM$Z$jTM3&<2)yp;is-!)6n9$w3D`(=62-n^a9GVR9HneH2Vv$`EqAEv~IzLkzRCna<`%YOUSU1uYc zH&)M$DEMnvw_o?2wt~o;`}<6n?q6qJd~2`T-&ge?Zp)l+x4C@np^o-UmM5*zR{viv zJz6ToW3PP7b=kqi{NA%_Boel7+}D0srfhbs@tw&c!7J31o_iP_5IAP|lk;DBZ2jUw zo5zwlrT=cXAC6_OWn20F&7rG>9Oqo@IM*b1ExYKwWX{7I-SvgDs*qXZ;s;KqbsB9ewvgh-Tyl= zVWwAg^{u&5anE~p)r$R^i^&ek~xHsb2ngK)1;RuB(OJ zPqPJQ{yDA{y}iz>|4D$;yuX`k<@G{4)8lTRH`*R8x_c`UoJ^xWCf%KRIQ^%wS;HY*?KDoCkOe0y#`(|LZ2-`44V9FN01@5dQ5 zWc3~{D*G5dS)zE7J;%@Jlh3PVv!*+pk#Xf;D!zkfYN_#b_sr}UJQq8Tw-|6by?l9W z#h3NEAzUKTIk~}#9%n{?zE_PoAy^%a zyGdcf=0}kmlz+UpQ1y#4TgJfg_1a{a(|}*Xt8^IXP>F=U;Mo z`T6*|{c2&^W3O+s()_)Hk8QliGW|;PFk{_)& z;!_O7S8cApbfrdO+WysB<=5VQ{8@h8`6%1}F(z}o|11>mT^`P}?AM2iPK}vGo9_2M zdA=tnyNyfAXS>F>^VvDtIsZ7fo>3HKn4vg7=isbBiN@LU&doRY+G8QXKZp6}*;Tp< zPp8Ob1S+0cdcQLCph(O0nDrsP(N0gC176)&H=%nk$FJobFZa~{GRP`9E4S%6k9Wvg zx68U+HuYL3TMoYwI(<7@^P5loE*>u8Qmo)BXFEqzirH z+&P0Rz22zxuMOpMdq2hD@J^*g$}d?>v!r&XaOd1E3h&iAwI(BFb;j1}iRUDOG%sX7 zE1A&!TP)LYn+dbQE9P&)U)YVgMf`KKygmm>?_D9RpUl!#?y)c+yv%v!yS2*oULRju z2tB=R`tHjLxbI4&(FXAJS~!a zyk=uMZ^eW6nbu5UPwPBZy}Y;eQp(KfH{6Q5tr((}g(ADwtUs~eJilB-XW^TJI*Ly@ zq&h`|?{LkqjEdv7pJ6;#Q+m?k74uxzpIcXd_fWRWyrTNtOSXZDHL<$0jFfWYU*^=h zI^KLO_-@g;G=T@Dmep13>Wlq6xdJ8b@UCASo^x))hFM2L3iB^Vtt*rb)>HCdXu12~ zTkA;k4pVcFO8ua$%dEcdZI2%R|8rKvo96$$C!ViAwkg;9nzySFSNnR7pND6~_-irQ z?_F{6Nqy}dp*)Y{Y_^U8OQVc=)6*t)OuLY5n0qPtSB{Kl|K{%NDT(jg=SSzfYf!$p z)^0--(7bDuLY&f=-x8-2d%Z}L;wYi9h_O8#xj z_u9(Xb&L7fIv;RxSR?RKOYP9t9!AbRx6eN z|CXKee+%e#30P$mq^K8WvB^$nN%H-h zdi%~_{$$klL;RN0_WC0-e8;Cd&)FqgvAq6H!(CSW_aP;%lRll4Q1f$I>h(j0y+Nz; zZs0SUhiZS`mGQJ*nKHGBO;Dh0-|Q(&_vPid{=QK#Rcrq_k;$njsrKCB_;tGRPg?Ev zGuo`3I(4$B^vOr(-#E=bJmp;6o0mOXHyCX5YP$-_5$&j+b)JejL? z?!w|Zbu+Jp*l!9I{~HxKXI8CRY(e3TmKW-2U!Eq3MzbdyA9X0Yt8bH5{*%`!q`M}JQ~iIZ&?9$XyJ%-L{*^;yQ!k0>6icz=RVO#do$tmZ2pc9K1ZPvKWcP;k5!rb?IQB&gg zMpo@DbDr4fJ72bX@AV`R{-e_m+}D1r6U=RIA^lveJF`#Vt6J;j>ThKpCAm)$eqEkq zcdqv9gfF&}wT6$4`m&fHw3CFbb85euvm|C}#*1u|ev3-yTH)+Ai|5bP=kf<8DtjBab2KiKvz)y+=H)uaLNTVg zrm!C`9i74!O$w{34|M7aE4m@E`Y>1i#ASZXizd!g7QN7>KcRHei$gwv z`QFVL2cDksnd;EXrG6#FZgRCzEXVrdZNKXKm?T+^nA~o)Na{N|!&1?r6J8UjJ zw$pMtF(X`&!yzWK#r_l9*CLs94L?uLjX0&`a7y6*ll=ZaZr%0PeU^2uKeG4mCHQ{i zt3N6*t3RS}W%*UBrCF8jCjVdq09MC%u-1iZ{Xap4Gewf6Zp zuhMR}HhkK}zPqSicI8a5SueBhe@OXOkd+y`-R$wnb=M5t6q0>*7i%?t{KGN(TTPep zv@J*HHVUp$-Jh>*VqMR%TVMPCtJTY@6=xPGN*c(|+Mcy!pW&Nt)tmH|ykv^_n<`j- z^?B;g?#2@>*8jLPUgTe?<}YEfV*fnp$oH<5vp7$ANogLd(vzRqFIAuZIc=T+Gb5MC z!ri<7-P`d)%l`)NKi>BJlLHntJc!xr)z2mHaq=PI6Kp&SJ#8n+${pDi^XT{G;CnI# zn-qUO?4J32A=hfzv;|eF&)6gnSiech)rZI$8v5mcKN)l%g3SE&ExNL$lG2Wy{JNIbSzy zxuF(T^F#FfRSAI=emf@5nWLtC?d{@Ib6HiUYMu4A(<^gX@kDv6#Q{5Zk=vVPmTPHU z-u;39osjkQMMrZE#$*?V?Jg4iFh^{e@tUay&9>|2*`NNRe(`G7qZ26_N^2YsIY)f1 z5AA(4XXUN4^K;kMW`Emrpy~#1#bjHN@GDl?x>f5VT&udXlnP1|raDh7et7BX!|dM% zrOlVm{y6kv+z+%KYP9;MUR!<4&)g~fQp&B@cMBV5p64t37t+tR>b<#Z z|L%p?mYp+Idv9DTw@>}KEw_a7{1-3YtIyPp^JlLQy*YP}Zgm@Dfy!Tw>Ebj0@y|c7 z?jVcNH^+eb;F?*QwXxe$m3AJaOlLliO$NN?(0`)f7d%HhxUfC&Q(Nhx(Hf)=*r|8hlHJ9JbJ}{v+GF@}pmwHLPO=|C=CzgG_ zdiv$9(le7TaLuWgTaeg$gkhOP@)jX&Yej2aV@u7%u#3N=!dIrh*4$bv9d&Po(smJU z#?z+`v9NGu&O8}u$jixodYwY=#S7oIzH~hK)zXjsN=#ef$`2o(3(M9{mAhQ7cK*#9 zo8oMr)M~A0v454bKOC=Nm}M_;@;-ato3FpqAJtCFG;sM?aW{BUUcKG1ms_j(Dksfq z{JeI`8^?_QCc$UYc^Hgj_7(A4IGj{&ul|P|oeSIJe~D_c z8`%FRh5T!mh?y~?HA>>USAu)(`gxmb&tFikn_oZUO}Nib;|U3K{wuaUa`y2x2;Wq{ z>R8zgg{P}pxV091`zyb!ORD>Y{;?PJ9Ulun`ndLXYHzr!ZWEi~9d>%gp3uMNZ+Ilv z&OOL_;D2#;a?r_hG4=niZ0LBPBgr0bohhIC>Ywik539c|H;=E0-}o{8x?ixs(Z>Rj zZWkYiT)Lv{&$P9A?G}4^`RIRhA78iGk~YVZe^33t!qlcyA3Y;ao%Anx<{ulLDL46| zX2!>}5AS`j3+|OKExF|5!;r9H-jb!wOGIX}Ju*0N@caGC)VE3OOYLH}-CH)jf8VuN z_8a#W8a~N!nk$|0;MK}( z-n>Zuf{IV#`)5p;;LUvF(nJ4m4UzwL^$BeFEz2pz85PgnV%55lss87x(3#WbuCX}0 zV_AF0jRPOAyFL>C@xkPJ!he~y>>7Vo^D(O?C#9q$Cn_!x54;v$wz1rZwMJD$U*|#O z>Gc20QvZFJ{-q&wZ>+}ym%pp(?;Hp?dE$$}6VsmC{|~TkjnOpCoM6i0bT_3lRxjnB z=ca{qu@CpmyCJ`-=+=Z9Mi+;rezsfqb3T8Ma%q_R-rz#*1h!58r)MsAV0q8@!L|PL zpZeoPH{x##-3UCoP~7f4-%aCpuiR`tDEKF^&ik-eYKd|~fZ*2jEQXQ|OPN|8$n35+ z^~>M9L;B@^1KzqBR@DdU+b`U@ajD|2VsPM+UQ4&=sLv19r1UJ*KO9@$cl==a^O_{F z*Af}I!gZPaH-7F<{{Ft}Yxn>De}7c>|8-olY0-c7cQc;ew3l)<=weK0ZTcn4!2MTM zfO*@0p)yAyHKBa|z?C7MCfu4yOOw+x#Zv2q>(9RH@xT2i`2nkfo-Bj)-~V6ktg^o0 ze3q+~^F`Ou(jDzCE@FF1{$EU2oFK=*F7W@q--@D*{j;*CyOnw|9F=}BQ)0E`!lXat zOjmUmT`~-4`1Idsvh&2NPY*0to21a%)bw-up8E`{EDq^nlSYeg~-`&-c<}fVEzYuczMCt1N3oV~ce56rgZ{x+~ zV9V_K&-(59-~Y?=1MXCGG%wU`ih8xTO0-X^@jCxym#vf5&-3^ZuVHbr;c#xnOmBs@ z{j~=fWor8wdKpwr7CgIlt+eH&jnS%WYTvJ2U)yuG`^#UMm+tiecFfm)%=X-G!zA+j z|8*OuBe%jn7<+!0zI}c3+y}{h5{FJN(wedEe9NxtSvhP>40+s+UwJGTzUj{u+VE@o zJ${!j_b-@jEiL%SGUth0iut+q9NzB1S9fKv<&`_Cp|19X+fVWJOxJbB)lV|`*MBV* zVPaI+(-0Z?KV0@6J6l6AQ@!s$+ZBJ-D=}v=Tsr$#K+ZBe>XzAxRWs`M`EE^L#;`~A ztXI$1s4dsTbXCtH-&QyNv*ybeed=A2t$qD&!=2r0)9y>YQQBhv^-UdP#d$T; zyW2K$eBPh5&UenE52ha;1>AUNKjZVH!kGQ_OusiPuutZzzWmUto2_%noa*_r+|A5v zS8tmC?(liNyvvW14n+CiKbpNN{r1c0?XOKbu3QaYRet;Y_oG|yRLoniwQ#G}6|HIe zZOm^!IHp%{YZ-5x@w&5_;j_2TJf0&y*THUsS$*JgqnGn`+iAM(&j`J$F@NK~4H+fJ z>gTQMKN_{IenVa(_sPaNZ?ate&*+J3%Xr4M_s7mGzfJDnQkI6?*nJ@PN@z*vy-6z; z)VzCR^yJCH8Fyw{iR(O_-!9N}F>0r#=P@_VrquV77DZb~zs3yO1;zF=4?zGoNz z^Tvd2?w`7DN(XFD|H~C;&GMb=iTI@b>HoOCt2flocRbn{dUir->iKMKmc;u$$2yjj z-p|$E)NLs8NUTd;We=lcoxGs6v%n$CWS%bdUtjpnvZt1}9AiHFN9?w~hP{@P#7XTs zGruRnX>La*?BVDU7SmA6sOR{r;jH}dbz|L~>s2d{@10tZa(K>#pZnBRR9uc6jb{qt z(|WJ6tfPO9bH1WX)Vuw&cNy_n^>`R1h%885@af!(HzkGvFP5+CYvNoY%m z;^d{Q@#dM{>r2nxzr3+0p3@#^Bu-UfZ+Nm<8^NH4fm-;)r<{~`C-gL?kkQ>C8sKmP5YU*r;y z^6aDE;y6bw^UKwHA3fvse8!Vxkzo9$v+TIR8~;;pzO~(wFWUdju>ae)M_1gPqdEJ=+%MUA>kkX+ue%;=^NwT7>!9S_Gae~!{I}8U zcY5LR#X5H*=FSxQm3*SXxlobIa7xgQn`c*EEM8;kb0o4V*!9MWjf)pAZC{*`@Z?E_40q}3m0uLe3u$~ zr|q}EwcoMl7tAh_i1|C8rT&26z9Vl%?K4l>Y?7AH&VT2Nd5T?O zZ_)b@TGc7#`D1%UNQ8%)#GhQfE1P`O*cjCOd=8}@IK3gjc}aMX(VT9!%mw^AqL{)N z**grj80<9Iw_*CE<&PU?CVV*YevU*$^S{P<+nWUXJdLm0bgf*J)vUJE;YCXQnrdg= zqRdlyOA?Q~&HMSyV#SN-mC0g7A~*gX+q>x9ob!3pIi5F2YED=Y+@KXDsdZHI<;Rzr zi)D4sTRgQ`lgy=2-NbxjcBSyc8NW{+Q4f#%G+mvYU2WpLOtyDAn_n|C&sr3*nD@hP z^UtR_d5UJL2}sU-;bPt4&UfaB#agKa9xC-;-YW@PZmHCASKab+vV}?Ns*IN!FE?jo zoz(bJr^UHVK_flWB6ZbmRglEyPJ9C-yY?YN)O7GtnGJEp1MoO zcWGdBQT0-d8y~0m7s#nC&OcK%_3F!4Q;Q9{{wan1{GZ<(@&@GkqlGMpSES?KHh*m$K}nYZ&=WHQ?hL7iZf*?F5iN*AN4*hl$n0` z=+Tqv5f^hec|6}!?H@MBC4+U@hpg~_=bO!c`?}qj=Uv)f=zOxR*VtUQvVKOd(k#n_ z#BWU>>U<`jXq)zN$)buapRJgT@;uX|1+^wnIJB6y2e5ks*CjC76RCA`n)kTj& zI*!caN;fzfupbsy1cQR#Zo*n~-fUrNgV)VxShw@JMuvGuzA(}3qruTHP? zOFi0tE%wih_?czpwTlcEFP%Nj@58l^6O3oGOgX}*GEL{%!9Z&$ zxpGZRGh)lf8X-fg=YO*!)|>r(DDJrbto}`A)ei54pQkfTTiNN%ezV}0O-wYpW| zcQ&60PFO9)TgYN&yLmy)vtuEaOC2=z+V45-essX8xPRrw)jN|U*JRE3J`;9;UDGF#S_A+tZ9FQmB zU1zd=XEKr$!BuC>91Gp>$2DTe{T5y z@vH3(!9%Zq&1Sq*QGdlS&)zvCB5jhc;jC@9=Y_aucb>VyX!Cz(m)W`W`eUz?8(YLR z(xeUh?iF=(EX?9BST;*H@Xzf_r%z>b)*ajA)55E?cV^8p-A`+(c$ZGskyV=@H^WOS zKjKF4Gpnr|*1rlqTwD5*Ns8%ktyTI`yY8&oS2jOaO@926gFhtH@BZxi$-jy=-hEbN z=vx0FJ7Fv99cA^Xyjgvll+r5Sy-@JVbyF*nZ1A~pYIsLG7eabWQN+EZ_lnpyVw1oFCyqWPeZ2xi9 z!(~;f*X`xY;>t26^c4jz6SLiu`l?=fdQ;;o$R4-5ea(Z`b9JSB5~nyfC%3%2!+AY>%j%hLoE`-f&eK^XBqQuMTU)YT zPAS7%r-5~AhM@e;b>4D6qFdVD96P(DCDys&u-9bs66WeBUsl|{mEWiQcjeXeAdZNa z^>;m-eg*_sX75yeeKs*6;CGSG9>+5a3KwmfTKMVBqhm~kBJHEXMPf8dhI)V$+DOJKl)WBI)8Cf&obFrtUd9)szs>k zX60#59d(~fQ8nCk;fdMKyPU5Y*_H@RZ?%-Z8KK2lFQeP(;`wY*yKc7QUsHbm#%P!N zRD;N@Io{kw-hFcO-futt{r>j%?5AJPb_rK?%_>^4WU67-{udg7cUZKgADY)$MF%FE zg>10=*q-9*YT0PXC;i8#H~M9Yld7V6LQ}5ZOR{}bldoLkydL3Is`m29iomRJN zSH&ATx zS=RWQ^>Wl)+`UBmbUV#mhWHBTnxGfW7+q4#9wnVn^~gm+&u*etRtp{@NSr_9Cm zZ#e%vxb6B=#KPRC!H+vBM!1Br>f){gPjx>$*}LgB-$L^)=BJGHhEKx}y03Mt&f6X_ z`@$U?&EMCV&cw8RpE1wZ`-a(D=lLhXZZ7;UP}qNJSp$3Y#rpR*YgO`6C)Tg2i@wq; zw>{$Dfx7bWuQ#`z{;;^Q&|~@Tw3c(b-{sln9dn!E5_dpT{Jr@;u3Y8YLZ2?IXgdCO z_Kvg5wcAL$LQYrsbvS3 z&tA>beDCy)gkwn}t+RKX{PMWh>Y4Xe6Nb%S7$2Lhox0y;=G_^5H=G|fe9v>((h#Wk z!LjCZMn13a`7om!&S`3E>}$@OPrbcyZE%v{o#ka;I%L=U);#ymwrAs1n};=1m<%@D zoX}Ul@}q$AhWK5t=o*XIaV2{(T$G~{q- zmAH{-Z_l6cG4jpDT$7Knm-6TRS9&wqzwK`JPtLk`J*-b>Ux*Ji%k%$tZRy8}XYE@K z?yOy!-(bJttf9mV>|E=*T-tlj+CQAVd`jwD+WmtYO$(h) zUQUrOp4GoH>(oq>IImf^-6x&z+i+vbqj^*N`u0R!NHEe?_debG+FbiZ_2jw=`{(Bu zah0>^{I6nu^Y*u`UZd>Pn|AYK7w(^M@3*aV-InKl8JJ^#h3piL~^a}OZTyo0cx-Q$MF_h4h`8_dN{%(w5KMzI^(6y*f$u)UXs@`9o6D zexhsNh1iyVf8^J*on>!!{Q)z(2i=c~D=k7_9qNgPO- z$j!fPCh%`vyrYWzHMNb8E%I|47*7jb&%9%CZjM4k+ree7w}Nyg&+<5`@BIIF-;#{e zn^c#_TJx;9$R_sw;m3;d^DocczWrjdzDgHsZ_DE=D_ta5vs%BOO}e@Bdrjjn!{Vjq zB_~yQuyTDokRGAmVsS`Wi?M6h2A|y=#!T_`nO6+^CoS&alKi>O<JT{t+eG9ZBM_@PVJjK4>?_td=1RSmK_RF~ko z{{p96E2Hl2niR&e*QO@|t@nm{=xpUUTpGV3woIAp^^)eLlR~xGuLeEc5n{V%{T}Pp zA?Nz;+eX%>oPW-cSSS$DdpL82X_U)@&5Eja-s?|p)IZqzQq`)F^;+lGyCx7v_^K6V)bGEQL=u8Xyv9tb}@aCPS>i=dwezRzu|E?FlF8{m! zc1+s7qUBxV8+m=vtA&g1U5a|@+jYUUi7fNid@@{7p=RT0>ruIkk+P-}yOpI$wWLRMqM23U?;$3%}sEH~CwGc2%0hN5Rxr+7e7LA1oYOwEt-=a}7Br z$gET^8`>w(t0ebU@t2b%Q+2EkgW0zX7t;r}RmBG*?HdxG=t_OIQ1J6#8U9G1M#{}D z?D?fn3)XBiyuc?Hb|L)rlt+K;K0SSb=YbsW82E2k=J4m$i?JMAbEV>0hHpn<*d@1>x>okJ zO$UxBc+6R<6DS+R)7mNAwd<-E>s`Lxsdu-;ZqJ{cmBww^EZ{uz_3D@B-e-%y@XPCQ zY}QWRkeId3v$IR~@unU-BVLwZw}MS~7r#s5V_zX9Sj)`4A#KsE%&Z5gZ-d|N`<8#} z?9Q21BI1Q2k@fa3_{87kObeOX$9QJRs#giucJ-B4D*WZQ3imiD?y7jQJ>>h078$uj zM?SudKkUD3yT8NM_2^&CsN8_B`eJXtiAr61d8%K`bz$qN^zEsO_pC47`)WJ;%hL^7 zySGIZ@^f`wN`G$kRg}f4VyQ<$3E%v9{@KPgg$AXoP5)NZve({0BY8;-wc+3`P_n9TM3eu#Cl@B0 z6-rOhQO-2J1RE{F{B}xWE7V;}zxKui5XFc6)y)qu^;&Xp{Xs6Zxip zPjzZ4vqIMQWiS82w>|UqhFZII+r>GV1x;8Pz=y~=UKWbs+urBRyvLB0!NFnw{_R<7 zIWqXR@9Ac~VaUp0>2PfS_O-#n`Fz`X&oG}dWMvR@c)5Q&qaJ4h-*)jLJ~f%`4f}*A zh_W&WIK1D#UEqpvBHwo7O?=8S+voig?h$2W&~UiEf4g8E%L>-*{V(}msBFJ~gL8+- z^qOFi2la1i*=s~Xzn2LJOjuU3WbMN3sa2eNrC;in6lQJOaMO32N(p0T5G%`+RZStR zD;J8t)KJ)4&hnBoXld>Db8p`HNLfF4aQeUUACXCCPo8^Yd;U)Kx#FmuAGS@tpSi6# zZS9-q^XBbpZdsdh(?_<*WA6T+ORjI4|7uSD_s+-0*Cqu#4ywNt_&{je>|04MnUolF zx0FQo=}$1W&z&K4dB@A0#hrIoe~-!0os}ANQT@`QYji0-5_$mAtW{w%GXbuUN75+^Fd{GV*&vk51fi z{*eE=*aG$Qyy0KH7cYBeXZFzp5+oR+`I_omO>)8Q$%tk3dKm-@G|I(o9)J=XC5Gvsc5d2D-l-_OT; zVlExsvQGH&>r-2T*0Py(mZoXS{L1*bR8lv~s?XHfU-{^x)3W!fv(_r#zOnxQyg z^Ilxab(eWjQ-5XU+@sHZ@BE3J*jh5}M0uYsZDnGhJNem5mrk+o#U%M?9b~Ev8<#sZf3ULk|W1i*Y}(@ zv^%}7FLkfHYFqw#^A~e7&sV)m`KEAn_R;M*fe%uapPjx+Kj!O-gC`yZ#>%@I990XS zA$ooDr0Q_Tq6773;>}~NFU9Efi0#~K`mF4m-bKZdznxQ$?{e>a%RTLe>h->;|K%sW z_twq2oRi-!`S85+9LwXqcR!v!vNfk@1G83PZo}vr~1AAjpW96bKT-| zE&6SD)@}Rp!$;g({BHEhUx(gY^_yc^pZMct|MZZT`<|S8FCTutVo}-Axwr4Ht-By- zRhNCX^!N067MA;1N;jw0Ec7n_o_6X_cK~$L?Y9L#K(QvZwzlz1tsHe|D0=vKZ;KH8ZyQtnS>o?M7MMuh^73t#MDU z)qnW>w9}odFJYJ5JCR@3y)8b!lir4xZr

dvoOcKDj@~zdzQO&yc)tP~XXFfA(Ba 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 hz5PP$ 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 delta 263817 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

N3Z3+eUOy6 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 diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index cb7495da8eb..c9fa8315e79 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -3,18 +3,56 @@ require 'spec_helper' feature 'Prioritize labels', feature: true do include WaitForAjax - context 'when project belongs to user' do - let(:user) { create(:user) } - let(:project) { create(:project, name: 'test', namespace: user.namespace) } + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } + let!(:feature) { create(:group_label, group: group, title: 'feature') } - scenario 'user can prioritize a label', js: true do - bug = create(:label, title: 'bug') - wontfix = create(:label, title: 'wontfix') - - project.labels << bug - project.labels << wontfix + context 'when user belongs to project team' do + before do + project.team << [user, :developer] login_as user + end + + scenario 'user can prioritize a group label', js: true do + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content('No prioritized labels yet') + + page.within('.other-labels') do + all('.js-toggle-priority')[1].click + wait_for_ajax + expect(page).not_to have_content('feature') + end + + page.within('.prioritized-labels') do + expect(page).not_to have_content('No prioritized labels yet') + expect(page).to have_content('feature') + end + end + + scenario 'user can unprioritize a group label', js: true do + create(:label_priority, project: project, label: feature, priority: 1) + + visit namespace_project_labels_path(project.namespace, project) + + page.within('.prioritized-labels') do + expect(page).to have_content('feature') + + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.other-labels') do + expect(page).to have_content('feature') + end + end + + scenario 'user can prioritize a project label', js: true do visit namespace_project_labels_path(project.namespace, project) expect(page).to have_content('No prioritized labels yet') @@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do end end - scenario 'user can unprioritize a label', js: true do - bug = create(:label, title: 'bug', priority: 1) - wontfix = create(:label, title: 'wontfix') - - project.labels << bug - project.labels << wontfix + scenario 'user can unprioritize a project label', js: true do + create(:label_priority, project: project, label: bug, priority: 1) - login_as user visit namespace_project_labels_path(project.namespace, project) - expect(page).to have_content('bug') - page.within('.prioritized-labels') do + expect(page).to have_content('bug') + first('.js-toggle-priority').click wait_for_ajax expect(page).not_to have_content('bug') @@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do end scenario 'user can sort prioritized labels and persist across reloads', js: true do - bug = create(:label, title: 'bug', priority: 1) - wontfix = create(:label, title: 'wontfix', priority: 2) - - project.labels << bug - project.labels << wontfix + create(:label_priority, project: project, label: bug, priority: 1) + create(:label_priority, project: project, label: feature, priority: 2) - login_as user visit namespace_project_labels_path(project.namespace, project) expect(page).to have_content 'bug' + expect(page).to have_content 'feature' expect(page).to have_content 'wontfix' # Sort labels - find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") + find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}") page.within('.prioritized-labels') do - expect(first('li')).to have_content('wontfix') + expect(first('li')).to have_content('feature') expect(page.all('li').last).to have_content('bug') end @@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do wait_for_ajax page.within('.prioritized-labels') do - expect(first('li')).to have_content('wontfix') + expect(first('li')).to have_content('feature') expect(page.all('li').last).to have_content('bug') end end @@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do context 'as a guest' do it 'does not prioritize labels' do - user = create(:user) guest = create(:user) - project = create(:project, name: 'test', namespace: user.namespace) - - create(:label, title: 'bug') login_as guest + visit namespace_project_labels_path(project.namespace, project) + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + expect(page).to have_content 'feature' expect(page).not_to have_css('.prioritized-labels') end end context 'as a non signed in user' do it 'does not prioritize labels' do - user = create(:user) - project = create(:project, name: 'test', namespace: user.namespace) - - create(:label, title: 'bug') - visit namespace_project_labels_path(project.namespace, project) + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + expect(page).to have_content 'feature' expect(page).not_to have_css('.prioritized-labels') end end diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb new file mode 100644 index 00000000000..27acc464ea2 --- /dev/null +++ b/spec/finders/labels_finder_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe LabelsFinder do + describe '#execute' do + let(:group_1) { create(:group) } + let(:group_2) { create(:group) } + let(:group_3) { create(:group) } + + let(:project_1) { create(:empty_project, namespace: group_1) } + let(:project_2) { create(:empty_project, namespace: group_2) } + let(:project_3) { create(:empty_project) } + let(:project_4) { create(:empty_project, :public) } + let(:project_5) { create(:empty_project, namespace: group_1) } + + let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') } + let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') } + let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') } + let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') } + + let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') } + let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') } + let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') } + + let(:user) { create(:user) } + + before do + create(:label, project: project_3, title: 'Label 3') + create(:group_label, group: group_3, title: 'Group Label 4') + + project_1.team << [user, :developer] + end + + context 'with no filter' do + it 'returns labels from projects the user have access' do + group_2.add_developer(user) + + finder = described_class.new(user) + + expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] + end + end + + context 'filtering by group_id' do + it 'returns labels available for any project within the group' do + group_1.add_developer(user) + + finder = described_class.new(user, group_id: group_1.id) + + expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5] + end + end + + context 'filtering by project_id' do + it 'returns labels available for the project' do + finder = described_class.new(user, project_id: project_1.id) + + expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1] + end + end + + context 'filtering by title' do + it 'returns label with that title' do + finder = described_class.new(user, title: 'Group Label 2') + + expect(finder.execute).to eq [group_label_2] + end + end + end +end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index f070fa3b254..8d94cf26ecb 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -13,7 +13,7 @@ "enum": ["backlog", "label", "done"] }, "label": { - "type": ["object"], + "type": ["object", "null"], "required": [ "id", "color", diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 501f150cfda..d30daf47543 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -5,27 +5,26 @@ describe LabelsHelper do let(:project) { create(:empty_project) } let(:label) { create(:label, project: project) } - context 'with @project set' do - before do - @project = project - end - - it 'uses the instance variable' do - expect(link_to_label(label)).to match %r{} + context 'without subject' do + it "uses the label's project" do + expect(link_to_label(label)).to match %r{.*} end end - context 'without @project set' do - it "uses the label's project" do - expect(link_to_label(label)).to match %r{.*} + context 'with a project as subject' do + let(:namespace) { build(:namespace, name: 'foo3') } + let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') } + + it 'links to project issues page' do + expect(link_to_label(label, subject: another_project)).to match %r{.*} end end - context 'with a project argument' do - let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') } + context 'with a group as subject' do + let(:group) { build(:group, name: 'bar') } - it 'links to merge requests page' do - expect(link_to_label(label, project: another_project)).to match %r{.*} + it 'links to group issues page' do + expect(link_to_label(label, subject: group)).to match %r{.*} end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 908ccebbf87..9c09f00ae8a 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end + describe 'group label references' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, name: 'gfm references', group: group) } + + context 'without project reference' do + let(:reference) { group_label.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}", project: project) + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: group_label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{group_label.name}\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with project reference' do + let(:reference) { project.to_reference + group_label.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}", project: project) + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: group_label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{group_label.name}\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + end + describe 'cross project label references' do context 'valid project referenced' do let(:another_project) { create(:empty_project, :public) } @@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end end + + describe 'cross group label references' do + context 'valid project referenced' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_group) { create(:group) } + let(:another_project) { create(:empty_project, :public, namespace: another_group) } + let(:project_name) { another_project.name_with_namespace } + let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } + let(:reference) { another_project.to_reference + group_label.to_reference } + + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}" + end + end + end end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 0af249d8690..f045463c1cb 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::Gfm::ReferenceRewriter do let(:text) { 'some text' } - let(:old_project) { create(:project) } - let(:new_project) { create(:project) } + let(:old_project) { create(:project, name: 'old') } + let(:new_project) { create(:project, name: 'new') } let(:user) { create(:user) } before { old_project.team << [user, :guest] } @@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } end - context 'description with labels' do + context 'description with project labels' do let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } let(:project_ref) { old_project.to_reference } @@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } end end + + context 'description with group labels' do + let(:old_group) { create(:group) } + let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) } + let(:project_ref) { old_project.to_reference } + + before do + old_project.update(namespace: old_group) + end + + context 'label referenced by id' do + let(:text) { '#1 and ~321' } + it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + end + + context 'label referenced by text' do + let(:text) { '#1 and ~"group label"' } + it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + end + end end context 'reference contains milestone' do diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 8854c8431b5..1af553f8f03 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -157,7 +157,7 @@ describe Gitlab::GithubImport::Importer, lib: true do { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } - ] + ] } described_class.new(project).execute diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index 54f85f8cffc..097861fd34d 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do subject { described_class.new(project) } before do + project.team << [project.creator, :master] project.create_import_data(data: import_data) end @@ -31,9 +32,9 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do subject.execute %w( - Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical - Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security - Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery + Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical + Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security + Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New ).each do |label| label.sub!("-", ": ") diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8fcbf12eab8..02b11bd999a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -38,6 +38,7 @@ label: - label_links - issues - merge_requests +- priorities milestone: - project - issues @@ -186,3 +187,5 @@ project: award_emoji: - awardable - user +priorities: +- label \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 98323fe6be4..ed9df468ced 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2,6 +2,21 @@ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "visibility_level": 10, "archived": false, + "labels": [ + { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + ] + } + ], "issues": [ { "id": 40, @@ -64,7 +79,37 @@ "updated_at": "2016-07-22T08:55:44.161Z", "template": false, "description": "", - "priority": null + "type": "ProjectLabel" + } + }, + { + "id": 3, + "label_id": 3, + "target_id": 40, + "target_type": "Issue", + "created_at": "2016-07-22T08:57:02.841Z", + "updated_at": "2016-07-22T08:57:02.841Z", + "label": { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] } } ], @@ -536,7 +581,7 @@ "updated_at": "2016-07-22T08:55:44.161Z", "template": false, "description": "", - "priority": null + "type": "ProjectLabel" } } ], @@ -2226,9 +2271,6 @@ } ] } - ], - "labels": [ - ], "milestones": [ { diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 7582a732cdf..069ea960321 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the same label associated to two issues' do restored_project_json - expect(Label.first.issues.count).to eq(2) + expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end it 'has milestones associated to two separate issues' do @@ -107,6 +107,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Label.first.label_links.first.target).not_to be_nil end + it 'has project labels' do + restored_project_json + + expect(ProjectLabel.count).to eq(2) + end + + it 'has no group labels' do + restored_project_json + + expect(GroupLabel.count).to eq(0) + end + + context 'with group' do + let!(:project) do + create(:empty_project, + name: 'project', + path: 'project', + builds_access_level: ProjectFeature::DISABLED, + issues_access_level: ProjectFeature::DISABLED, + group: create(:group)) + end + + it 'has group labels' do + restored_project_json + + expect(GroupLabel.count).to eq(1) + end + + it 'has label priorities' do + restored_project_json + + expect(GroupLabel.first.priorities).not_to be_empty + end + end + it 'has a project feature' do restored_project_json diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index cf8f2200c57..c8bba553558 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -111,6 +111,18 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty end + it 'has project and group labels' do + label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']} + + expect(label_types).to match_array(['ProjectLabel', 'GroupLabel']) + end + + it 'has priorities associated to labels' do + priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']} + + expect(priorities.flatten).not_to be_empty + end + it 'saves the correct service type' do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end @@ -135,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do issue = create(:issue, assignee: user) snippet = create(:project_snippet) release = create(:release) + group = create(:group) project = create(:project, :public, issues: [issue], snippets: [snippet], - releases: [release] + releases: [release], + group: group ) - label = create(:label, project: project) - create(:label_link, label: label, target: issue) + project_label = create(:label, project: project) + group_label = create(:group_label, group: group) + create(:label_link, label: project_label, target: issue) + create(:label_link, label: group_label, target: issue) + create(:label_priority, label: group_label, priority: 1) milestone = create(:milestone, project: project) merge_request = create(:merge_request, source_project: project, milestone: milestone) commit_status = create(:commit_status, project: project) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 8c8be66df9f..feee0f025d8 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -60,11 +60,13 @@ LabelLink: - target_type - created_at - updated_at -Label: +ProjectLabel: - id - title - color +- group_id - project_id +- type - created_at - updated_at - template @@ -329,3 +331,10 @@ AwardEmoji: - awardable_type - created_at - updated_at +LabelPriority: +- id +- project_id +- label_id +- priority +- created_at +- updated_at \ No newline at end of file diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb new file mode 100644 index 00000000000..85eb889225b --- /dev/null +++ b/spec/models/group_label_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe GroupLabel, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:group) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group) } + end + + describe '#subject' do + it 'aliases group to subject' do + subject = described_class.new(group: build(:group)) + + expect(subject.subject).to be(subject.group) + end + end + + describe '#to_reference' do + let(:label) { create(:group_label) } + + context 'using id' do + it 'returns a String reference to the object' do + expect(label.to_reference).to eq "~#{label.id}" + end + end + + context 'using name' do + it 'returns a String reference to the object' do + expect(label.to_reference(format: :name)).to eq %(~"#{label.name}") + end + + it 'uses id when name contains double quote' do + label = create(:label, name: %q{"irony"}) + expect(label.to_reference(format: :name)).to eq "~#{label.id}" + end + end + + context 'using invalid format' do + it 'raises error' do + expect { label.to_reference(format: :invalid) } + .to raise_error StandardError, /Unknown format/ + end + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0b3ef9b98fd..ac862055ebc 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -12,6 +12,7 @@ describe Group, models: true do it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } + it { is_expected.to have_many(:labels).class_name('GroupLabel') } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb new file mode 100644 index 00000000000..d18c2f7949a --- /dev/null +++ b/spec/models/label_priority_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe LabelPriority, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:label) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:label) } + it { is_expected.to validate_numericality_of(:priority).only_integer.is_greater_than_or_equal_to(0) } + + it 'validates uniqueness of label_id scoped to project_id' do + create(:label_priority) + + expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id) + end + end +end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 5a5d1a5d60c..0c163659a71 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -1,46 +1,42 @@ require 'spec_helper' describe Label, models: true do - let(:label) { create(:label) } + describe 'modules' do + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Subscribable) } + end describe 'associations' do - it { is_expected.to belong_to(:project) } - - it { is_expected.to have_many(:label_links).dependent(:destroy) } it { is_expected.to have_many(:issues).through(:label_links).source(:target) } + it { is_expected.to have_many(:label_links).dependent(:destroy) } it { is_expected.to have_many(:lists).dependent(:destroy) } - end - - describe 'modules' do - subject { described_class } - - it { is_expected.to include_module(Referable) } + it { is_expected.to have_many(:priorities).class_name('LabelPriority') } end describe 'validation' do - it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_uniqueness_of(:title).scoped_to([:group_id, :project_id]) } it 'validates color code' do - expect(label).not_to allow_value('G-ITLAB').for(:color) - expect(label).not_to allow_value('AABBCC').for(:color) - expect(label).not_to allow_value('#AABBCCEE').for(:color) - expect(label).not_to allow_value('GGHHII').for(:color) - expect(label).not_to allow_value('#').for(:color) - expect(label).not_to allow_value('').for(:color) - - expect(label).to allow_value('#AABBCC').for(:color) - expect(label).to allow_value('#abcdef').for(:color) + is_expected.not_to allow_value('G-ITLAB').for(:color) + is_expected.not_to allow_value('AABBCC').for(:color) + is_expected.not_to allow_value('#AABBCCEE').for(:color) + is_expected.not_to allow_value('GGHHII').for(:color) + is_expected.not_to allow_value('#').for(:color) + is_expected.not_to allow_value('').for(:color) + + is_expected.to allow_value('#AABBCC').for(:color) + is_expected.to allow_value('#abcdef').for(:color) end it 'validates title' do - expect(label).not_to allow_value('G,ITLAB').for(:title) - expect(label).not_to allow_value('').for(:title) - - expect(label).to allow_value('GITLAB').for(:title) - expect(label).to allow_value('gitlab').for(:title) - expect(label).to allow_value('G?ITLAB').for(:title) - expect(label).to allow_value('G&ITLAB').for(:title) - expect(label).to allow_value("customer's request").for(:title) + is_expected.not_to allow_value('G,ITLAB').for(:title) + is_expected.not_to allow_value('').for(:title) + + is_expected.to allow_value('GITLAB').for(:title) + is_expected.to allow_value('gitlab').for(:title) + is_expected.to allow_value('G?ITLAB').for(:title) + is_expected.to allow_value('G&ITLAB').for(:title) + is_expected.to allow_value("customer's request").for(:title) end end @@ -51,45 +47,59 @@ describe Label, models: true do end end - describe '#to_reference' do - context 'using id' do - it 'returns a String reference to the object' do - expect(label.to_reference).to eq "~#{label.id}" - end - end + describe 'priorization' do + subject(:label) { create(:label) } - context 'using name' do - it 'returns a String reference to the object' do - expect(label.to_reference(format: :name)).to eq %(~"#{label.name}") + let(:project) { label.project } + + describe '#prioritize!' do + context 'when label is not prioritized' do + it 'creates a label priority' do + expect { label.prioritize!(project, 1) }.to change(label.priorities, :count).by(1) + end + + it 'sets label priority' do + label.prioritize!(project, 1) + + expect(label.priorities.first.priority).to eq 1 + end end - it 'uses id when name contains double quote' do - label = create(:label, name: %q{"irony"}) - expect(label.to_reference(format: :name)).to eq "~#{label.id}" + context 'when label is prioritized' do + let!(:priority) { create(:label_priority, project: project, label: label, priority: 0) } + + it 'does not create a label priority' do + expect { label.prioritize!(project, 1) }.not_to change(label.priorities, :count) + end + + it 'updates label priority' do + label.prioritize!(project, 1) + + expect(priority.reload.priority).to eq 1 + end end end - context 'using invalid format' do - it 'raises error' do - expect { label.to_reference(format: :invalid) } - .to raise_error StandardError, /Unknown format/ + describe '#unprioritize!' do + it 'removes label priority' do + create(:label_priority, project: project, label: label, priority: 0) + + expect { label.unprioritize!(project) }.to change(label.priorities, :count).by(-1) end end - context 'cross project reference' do - let(:project) { create(:project) } - - context 'using name' do - it 'returns cross reference with label name' do - expect(label.to_reference(project, format: :name)) - .to eq %Q(#{label.project.to_reference}~"#{label.name}") + describe '#priority' do + context 'when label is not prioritized' do + it 'returns nil' do + expect(label.priority(project)).to be_nil end end - context 'using id' do - it 'returns cross reference with label id' do - expect(label.to_reference(project, format: :id)) - .to eq %Q(#{label.project.to_reference}~#{label.id}) + context 'when label is prioritized' do + it 'returns label priority' do + create(:label_priority, project: project, label: label, priority: 1) + + expect(label.priority(project)).to eq 1 end end end diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb new file mode 100644 index 00000000000..18c9d449ee5 --- /dev/null +++ b/spec/models/project_label_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe ProjectLabel, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + + context 'validates if title must not exist at group level' do + let(:group) { create(:group, name: 'gitlab-org') } + let(:project) { create(:empty_project, group: group) } + + before do + create(:group_label, group: group, title: 'Bug') + end + + it 'returns error if title already exists at group level' do + label = described_class.new(project: project, title: 'Bug') + + label.valid? + + expect(label.errors[:title]).to include 'already exists at group level for gitlab-org. Please choose another one.' + end + + it 'does not returns error if title does not exist at group level' do + label = described_class.new(project: project, title: 'Security') + + label.valid? + + expect(label.errors[:title]).to be_empty + end + + it 'does not returns error if project does not belong to group' do + another_project = create(:empty_project) + label = described_class.new(project: another_project, title: 'Bug') + + label.valid? + + expect(label.errors[:title]).to be_empty + end + + it 'does not returns error when title does not change' do + project_label = create(:label, project: project, name: 'Security') + create(:group_label, group: group, name: 'Security') + project_label.description = 'Security related stuff.' + + project_label.valid? + + expect(project_label.errors[:title]).to be_empty + end + end + + context 'when attempting to add more than one priority to the project label' do + it 'returns error' do + subject.priorities.build + subject.priorities.build + + subject.valid? + + expect(subject.errors[:priorities]).to include 'Number of permitted priorities exceeded' + end + end + end + + describe '#subject' do + it 'aliases project to subject' do + subject = described_class.new(project: build(:empty_project)) + + expect(subject.subject).to be(subject.project) + end + end + + describe '#to_reference' do + let(:label) { create(:label) } + + context 'using id' do + it 'returns a String reference to the object' do + expect(label.to_reference).to eq "~#{label.id}" + end + end + + context 'using name' do + it 'returns a String reference to the object' do + expect(label.to_reference(format: :name)).to eq %(~"#{label.name}") + end + + it 'uses id when name contains double quote' do + label = create(:label, name: %q{"irony"}) + expect(label.to_reference(format: :name)).to eq "~#{label.id}" + end + end + + context 'using invalid format' do + it 'raises error' do + expect { label.to_reference(format: :invalid) } + .to raise_error StandardError, /Unknown format/ + end + end + + context 'cross project reference' do + let(:project) { create(:project) } + + context 'using name' do + it 'returns cross reference with label name' do + expect(label.to_reference(project, format: :name)) + .to eq %Q(#{label.project.to_reference}~"#{label.name}") + end + end + + context 'using id' do + it 'returns cross reference with label id' do + expect(label.to_reference(project, format: :id)) + .to eq %Q(#{label.project.to_reference}~#{label.id}) + end + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 67dbcc362f6..e6d98e25d0b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -56,7 +56,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } - it { is_expected.to have_many(:labels).dependent(:destroy) } + it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) } it { is_expected.to have_many(:users_star_projects).dependent(:destroy) } it { is_expected.to have_many(:environments).dependent(:destroy) } it { is_expected.to have_many(:deployments).dependent(:destroy) } diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index f4b04445c6c..4f5c09a3029 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -106,9 +106,20 @@ describe API::API, api: true do describe "POST /projects/:id/board/lists" do let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - it 'creates a new issue board list' do - post api(base_url, user), - label_id: ux_label.id + it 'creates a new issue board list for group labels' do + group = create(:group) + group_label = create(:group_label, group: group) + project.update(group: group) + + post api(base_url, user), label_id: group_label.id + + expect(response).to have_http_status(201) + expect(json_response['label']['name']).to eq(group_label.title) + expect(json_response['position']).to eq(3) + end + + it 'creates a new issue board list for project labels' do + post api(base_url, user), label_id: ux_label.id expect(response).to have_http_status(201) expect(json_response['label']['name']).to eq(ux_label.title) @@ -116,15 +127,13 @@ describe API::API, api: true do end it 'returns 400 when creating a new list if label_id is invalid' do - post api(base_url, user), - label_id: 23423 + post api(base_url, user), label_id: 23423 expect(response).to have_http_status(400) end - it "returns 403 for project members with guest role" do - put api("#{base_url}/#{test_list.id}", guest), - position: 1 + it 'returns 403 for project members with guest role' do + put api("#{base_url}/#{test_list.id}", guest), position: 1 expect(response).to have_http_status(403) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 83789223019..1da9988978b 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -12,12 +12,18 @@ describe API::API, api: true do end describe 'GET /projects/:id/labels' do - it 'returns project labels' do + it 'returns all available labels to the project' do + group = create(:group) + group_label = create(:group_label, group: group) + project.update(group: group) + get api("/projects/#{project.id}/labels", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['name']).to eq(label1.name) + expect(json_response.size).to eq(2) + expect(json_response.first['name']).to eq(group_label.name) + expect(json_response.second['name']).to eq(label1.name) end end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index e7806add916..a7e9efcf93f 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -9,6 +9,10 @@ describe Boards::Lists::CreateService, services: true do subject(:service) { described_class.new(project, user, label_id: label.id) } + before do + project.team << [user, :developer] + end + context 'when board lists is empty' do it 'creates a new list at beginning of the list' do list = service.execute(board) diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb index 8b2f5e81338..ed0337662af 100644 --- a/spec/services/boards/lists/generate_service_spec.rb +++ b/spec/services/boards/lists/generate_service_spec.rb @@ -8,6 +8,10 @@ describe Boards::Lists::GenerateService, services: true do subject(:service) { described_class.new(project, user) } + before do + project.team << [user, :developer] + end + context 'when board lists is empty' do it 'creates the default lists' do expect { service.execute(board) }.to change(board.lists, :count).by(2) diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 1050502fa19..5c0331ebe66 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -67,6 +67,27 @@ describe Issues::CreateService, services: true do expect(Todo.where(attributes).count).to eq 1 end + context 'when label belongs to project group' do + let(:group) { create(:group) } + let(:group_labels) { create_pair(:group_label, group: group) } + + let(:opts) do + { + title: 'Title', + description: 'Description', + label_ids: group_labels.map(&:id) + } + end + + before do + project.update(group: group) + end + + it 'assigns group labels' do + expect(issue.labels).to match_array group_labels + end + end + context 'when label belongs to different project' do let(:label) { create(:label) } diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb new file mode 100644 index 00000000000..cbfc63de811 --- /dev/null +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Labels::FindOrCreateService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + + let(:params) do + { + title: 'Security', + description: 'Security related stuff.', + color: '#FF0000' + } + end + + subject(:service) { described_class.new(user, project, params) } + + before do + project.team << [user, :developer] + end + + context 'when label does not exist at group level' do + it 'creates a new label at project level' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end + + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') + + expect(service.execute).to eq group_label + end + end + + context 'when label does not exist at group level' do + it 'creates a new label at project leve' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end + + context 'when label exists at project level' do + it 'returns the project label' do + project_label = create(:label, project: project, title: 'Security') + + expect(service.execute).to eq project_label + end + end + end +end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb new file mode 100644 index 00000000000..ddf3527dc0f --- /dev/null +++ b/spec/services/labels/transfer_service_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Labels::TransferService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:group_1) { create(:group) } + let(:group_2) { create(:group) } + let(:group_3) { create(:group) } + let(:project_1) { create(:project, namespace: group_2) } + let(:project_2) { create(:project, namespace: group_3) } + + let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') } + let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') } + let(:group_label_3) { create(:group_label, group: group_1, name: 'Group Label 3') } + let(:group_label_4) { create(:group_label, group: group_2, name: 'Group Label 4') } + let(:group_label_5) { create(:group_label, group: group_3, name: 'Group Label 5') } + let(:project_label_1) { create(:label, project: project_1, name: 'Project Label 1') } + + subject(:service) { described_class.new(user, group_1, project_1) } + + before do + create(:labeled_issue, project: project_1, labels: [group_label_1]) + create(:labeled_issue, project: project_1, labels: [group_label_4]) + create(:labeled_issue, project: project_1, labels: [project_label_1]) + create(:labeled_issue, project: project_2, labels: [group_label_5]) + create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2]) + create(:labeled_merge_request, source_project: project_2, labels: [group_label_5]) + end + + it 'recreates the missing group labels at project level' do + expect { service.execute }.to change(project_1.labels, :count).by(2) + end + + it 'recreates label priorities related to the missing group labels' do + create(:label_priority, project: project_1, label: group_label_1, priority: 1) + + service.execute + + new_project_label = project_1.labels.find_by(title: group_label_1.title) + expect(new_project_label.id).not_to eq group_label_1.id + expect(new_project_label.priorities).not_to be_empty + end + + it 'does not recreate missing group labels that are not applied to issues or merge requests' do + service.execute + + expect(project_1.labels.where(title: group_label_3.title)).to be_empty + end + + it 'does not recreate missing group labels that already exist in the project group' do + service.execute + + expect(project_1.labels.where(title: group_label_4.title)).to be_empty + end + end +end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 57c71544dff..1540b90163a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } end end + + context 'missing group labels applied to issues or merge requests' do + it 'delegates tranfer to Labels::TransferService' do + group.add_owner(user) + + expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original + + transfer_project(project, user, group) + end + end end -- GitLab From 74974f023af3b7219f3267aa58ceac711aee0cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 19 Oct 2016 07:53:05 +0000 Subject: [PATCH 010/109] Merge branch '22191-delete-dynamic-envs-mr' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete dynamic environments - Adds "close environment" action to a merge request - Adds tabs to environments list - Adds close button to each environment row in environments list - Replaces Destroy button with Close button inside an environment - Adds close button to builds list inside an environment In order to enable stopping environments a valid `.gitlab-ci.yml` syntax has to be used: ``` review: environment: name: review/$app on_stop: stop_review stop_review: script: echo Delete My App when: manual environment: name: review/$app action: stop ``` This MR requires that `stop_review` has to have: `when`, `environment:name` and `environment:action` defined. The next MR after this one will verify that and enforce that these settings are configured. It will also implicitly configure these settings, making it possible to define it like this: ``` review: environment: name: review/$app on_stop: stop_review stop_review: script: echo Delete My App ``` Closes #22191 See merge request !6669 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + .../javascripts/merge_request_widget.js.es6 | 11 ++ .../stylesheets/pages/environments.scss | 8 ++ .../stylesheets/pages/merge_requests.scss | 9 ++ .../projects/environments_controller.rb | 25 ++-- .../projects/merge_requests_controller.rb | 6 + app/models/deployment.rb | 13 +- app/models/environment.rb | 28 +++++ app/models/project.rb | 2 +- app/services/create_deployment_service.rb | 21 +++- app/views/projects/deployments/_actions.haml | 41 +++---- .../deployments/_deployment.html.haml | 4 +- app/views/projects/deployments/_rollback.haml | 6 + .../environments/_environment.html.haml | 6 +- .../environments/_external_url.html.haml | 3 + .../projects/environments/_stop.html.haml | 5 + .../projects/environments/index.html.haml | 19 ++- .../projects/environments/show.html.haml | 6 +- config/routes/project.rb | 6 +- db/fixtures/development/14_pipelines.rb | 3 +- ...20161006104309_add_state_to_environment.rb | 15 +++ ...1017095000_add_properties_to_deployment.rb | 9 ++ db/schema.rb | 2 + lib/ci/gitlab_ci_yaml_processor.rb | 30 +++++ lib/gitlab/ci/config/node/environment.rb | 18 ++- spec/features/environments_spec.rb | 112 +++++++++++++----- .../merge_when_build_succeeds_spec.rb | 2 +- .../merge_requests/widget_deployments_spec.rb | 53 +++++++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 48 +++++++- .../gitlab/ci/config/node/environment_spec.rb | 64 +++++++++- spec/models/deployment_spec.rb | 46 +++++++ spec/models/environment_spec.rb | 70 +++++++++++ .../create_deployment_service_spec.rb | 65 +++++++++- 33 files changed, 656 insertions(+), 101 deletions(-) create mode 100644 app/views/projects/deployments/_rollback.haml create mode 100644 app/views/projects/environments/_external_url.html.haml create mode 100644 app/views/projects/environments/_stop.html.haml create mode 100644 db/migrate/20161006104309_add_state_to_environment.rb create mode 100644 db/migrate/20161017095000_add_properties_to_deployment.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 739c06baf14..11100044cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix a typo in doc/api/labels.md - API: all unknown routing will be handled with 404 Not Found - Add docs for request profiling + - Delete dynamic environments - Make guests unable to view MRs on private projects ## 8.12.7 diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index fcadc4bc515..3ff6851d59b 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -17,6 +17,12 @@ View on <%- external_url_formatted %> + + + + Stop environment + + `; @@ -205,6 +211,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); + + if (!environment.stop_url) { + $('.js-stop-env-link', $template).remove(); + } + if (environment.deployed_at && environment.deployed_at_formatted) { environment.deployed_at = $.timeago(environment.deployed_at) + '.'; } else { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 820cc0fc991..12ee0a5dc3d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -38,6 +38,14 @@ color: $gl-dark-link-color; } + .stop-env-link { + color: $table-text-gray; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment { .build-column { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 101472278e2..35a1877df95 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -183,6 +183,15 @@ .ci-coverage { float: right; } + + .stop-env-container { + color: $gl-text-color; + float: right; + + a { + color: $gl-text-color; + } + } } .mr_source_commit, diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 58678f96879..ea22b2dcc15 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :update, :destroy] - before_action :environment, only: [:show, :edit, :update, :destroy] + before_action :authorize_create_deployment!, only: [:stop] + before_action :authorize_update_environment!, only: [:edit, :update] + before_action :environment, only: [:show, :edit, :update, :stop] def index - @environments = project.environments + @scope = params[:scope] + @all_environments = project.environments + @environments = + if @scope == 'stopped' + @all_environments.stopped + else + @all_environments.available + end end def show @@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end - def destroy - if @environment.destroy - flash[:notice] = 'Environment was successfully removed.' - else - flash[:alert] = 'Failed to remove environment.' - end + def stop + return render_404 unless @environment.stoppable? - redirect_to namespace_project_environments_path(project.namespace, project) + new_action = @environment.stop!(current_user) + redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) end private diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0c7411bb61d..0f593d1a936 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -425,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController project = environment.project deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + stop_url = + if environment.stoppable? && can?(current_user, :create_deployment, environment) + stop_namespace_project_environment_path(project.namespace, project, environment) + end + { id: environment.id, name: environment.name, url: namespace_project_environment_path(project.namespace, project, environment), + stop_url: stop_url, external_url: environment.external_url, external_url_formatted: environment.formatted_external_url, deployed_at: deployment.try(:created_at), diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 3d9902d496e..1f8c5fb3d85 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base end def manual_actions - deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_actions) end def includes_commit?(commit) @@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base take end + def stop_action + return nil unless on_stop.present? + return nil unless manual_actions + + @stop_action ||= manual_actions.find_by(name: on_stop) + end + + def stoppable? + stop_action.present? + end + def formatted_deployment_time created_at.to_time.in_time_zone.to_s(:medium) end diff --git a/app/models/environment.rb b/app/models/environment.rb index d970bc0a005..d575f1dc73a 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base allow_nil: true, addressable_url: true + delegate :stop_action, to: :last_deployment, allow_nil: true + + scope :available, -> { with_state(:available) } + scope :stopped, -> { with_state(:stopped) } + + state_machine :state, initial: :available do + event :start do + transition stopped: :available + end + + event :stop do + transition available: :stopped + end + + state :available + state :stopped + end + def last_deployment deployments.last end @@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base external_url.gsub(/\A.*?:\/\//, '') end + + def stoppable? + available? && stop_action.present? + end + + def stop!(current_user) + return unless stoppable? + + stop_action.play(current_user) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 852c345c9b9..a6039bb8cc4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1295,7 +1295,7 @@ class Project < ActiveRecord::Base environment_ids.where(ref: ref) end - environments.where(id: environment_ids).select do |environment| + environments.available.where(id: environment_ids).select do |environment| environment.includes_commit?(commit) end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index ff9a8310a8c..8ae15ad32f4 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService ActiveRecord::Base.transaction do @deployable = deployable - @environment = prepare_environment + + @environment = environment + @environment.external_url = expanded_url if expanded_url + @environment.fire_state_event(action) + + return unless @environment.save + return if @environment.stopped? deploy.tap do |deployment| deployment.update_merge_request_metrics! @@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService tag: params[:tag], sha: params[:sha], user: current_user, - deployable: @deployable) + deployable: @deployable, + on_stop: options[:on_stop]) end - def prepare_environment - project.environments.find_or_create_by(name: expanded_name) do |environment| - environment.external_url = expanded_url - end + def environment + @environment ||= project.environments.find_or_create_by(name: expanded_name) end def expanded_name @@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService def variables params[:variables] || [] end + + def action + options[:action] || 'start' + end end diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 22c4a75d213..58a214bdbd1 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,28 +1,15 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable - .pull-right - - - external_url = deployment.environment.external_url - - if external_url - = link_to external_url, target: '_blank', class: 'btn external-url' do - = icon('external-link') - - - actions = deployment.manual_actions - - if actions.present? - .inline - .dropdown - %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = custom_icon('icon_play') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - actions.each do |action| - %li - = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = custom_icon('icon_play') - %span= action.name.humanize +- if can?(current_user, :create_deployment, deployment) + - actions = deployment.manual_actions + - if actions.present? + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + = custom_icon('icon_play') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - actions.each do |action| + %li + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do + = custom_icon('icon_play') + %span= action.name.humanize - - if local_assigns.fetch(:allow_rollback, false) - = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - - if deployment.last? - Re-deploy - - else - Rollback diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index ca0005abd0c..9238f232c7e 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,4 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true + .pull-right + = render 'projects/deployments/actions', deployment: deployment + = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml new file mode 100644 index 00000000000..5941e01c6f1 --- /dev/null +++ b/app/views/projects/deployments/_rollback.haml @@ -0,0 +1,6 @@ +- if can?(current_user, :create_deployment, deployment) && deployment.deployable + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do + - if deployment.last? + Re-deploy + - else + Rollback diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 251694e897c..b75d5df4150 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -28,4 +28,8 @@ #{time_ago_with_tooltip(last_deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: last_deployment + .pull-right + = render 'projects/environments/external_url', environment: environment + = render 'projects/deployments/actions', deployment: last_deployment + = render 'projects/environments/stop', environment: environment + = render 'projects/deployments/rollback', deployment: last_deployment diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml new file mode 100644 index 00000000000..4c8fe1c271b --- /dev/null +++ b/app/views/projects/environments/_external_url.html.haml @@ -0,0 +1,3 @@ +- if environment.external_url && can?(current_user, :read_environment, environment) + = link_to environment.external_url, target: '_blank', class: 'btn external-url' do + = icon('external-link') diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml new file mode 100644 index 00000000000..69848123c17 --- /dev/null +++ b/app/views/projects/environments/_stop.html.haml @@ -0,0 +1,5 @@ +- if can?(current_user, :create_deployment, environment) && environment.stoppable? + .inline + = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post, + class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do + = icon('stop', class: 'stop-env-icon') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 721ba156334..8f555afcf11 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,14 +3,27 @@ = render "projects/pipelines/head" %div{ class: container_class } - - if can?(current_user, :create_environment, @project) && !@environments.blank? - .top-area + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_environments_path(@project) do + Available + %span.badge.js-available-environments-count + = number_with_delimiter(@all_environments.available.count) + + %li{class: ('active' if @scope == 'stopped')} + = link_to project_environments_path(@project, scope: :stopped) do + Stopped + %span.badge.js-stopped-environments-count + = number_with_delimiter(@all_environments.stopped.count) + + - if can?(current_user, :create_environment, @project) && !@all_environments.blank? .nav-controls = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment .environments-container - - if @environments.blank? + - if @all_environments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title You don't have any environments right now. diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 90c59223a35..bcac73d3698 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -3,14 +3,16 @@ = render "projects/pipelines/head" %div{ class: container_class } - .top-area + .top-area.adjust .col-md-9 %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete + - if can?(current_user, :create_deployment, @environment) && @environment.stoppable? + = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .deployments-container - if @deployments.blank? diff --git a/config/routes/project.rb b/config/routes/project.rb index 711a59df744..8142e231621 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -319,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end - resources :environments + resources :environments, except: [:destroy] do + member do + post :stop + end + end resource :cycle_analytics, only: [:show] diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 803cbca584d..08ad3097d34 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, - { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success }, + { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } }, + { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, { name: 'slack', stage: 'notify', when: 'manual', status: :created }, ] diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb new file mode 100644 index 00000000000..ccb546654f9 --- /dev/null +++ b/db/migrate/20161006104309_add_state_to_environment.rb @@ -0,0 +1,15 @@ +class AddStateToEnvironment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:environments, :state, :string, default: :available) + end + + def down + remove_column(:environments, :state) + end +end diff --git a/db/migrate/20161017095000_add_properties_to_deployment.rb b/db/migrate/20161017095000_add_properties_to_deployment.rb new file mode 100644 index 00000000000..f620ee0de1c --- /dev/null +++ b/db/migrate/20161017095000_add_properties_to_deployment.rb @@ -0,0 +1,9 @@ +class AddPropertiesToDeployment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :deployments, :on_stop, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index ce2a7752625..65f55aa109b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161018024550) do t.string "deployable_type" t.datetime "created_at" t.datetime "updated_at" + t.string "on_stop" end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree @@ -404,6 +405,7 @@ ActiveRecord::Schema.define(version: 20161018024550) do t.datetime "updated_at" t.string "external_url" t.string "environment_type" + t.string "state", default: "available", null: false end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 2fd1fced65c..3e33c9399e2 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -109,6 +109,7 @@ module Ci validate_job_stage!(name, job) validate_job_dependencies!(name, job) + validate_job_environment!(name, job) end end @@ -150,6 +151,35 @@ module Ci end end + def validate_job_environment!(name, job) + return unless job[:environment] + return unless job[:environment].is_a?(Hash) + + environment = job[:environment] + validate_on_stop_job!(name, environment, environment[:on_stop]) + end + + def validate_on_stop_job!(name, environment, on_stop) + return unless on_stop + + on_stop_job = @jobs[on_stop.to_sym] + unless on_stop_job + raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" + end + + unless on_stop_job[:environment] + raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" + end + + unless on_stop_job[:environment][:name] == environment[:name] + raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" + end + + unless on_stop_job[:environment][:action] == 'stop' + raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" + end + end + def process?(only_params, except_params, ref, tag, trigger_request) if only_params.present? return false unless matching?(only_params, ref, tag, trigger_request) diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb index d388ab6b879..9a95ef43628 100644 --- a/lib/gitlab/ci/config/node/environment.rb +++ b/lib/gitlab/ci/config/node/environment.rb @@ -8,7 +8,7 @@ module Gitlab class Environment < Entry include Validatable - ALLOWED_KEYS = %i[name url] + ALLOWED_KEYS = %i[name url action on_stop] validations do validate do @@ -35,6 +35,12 @@ module Gitlab length: { maximum: 255 }, addressable_url: true, allow_nil: true + + validates :action, + inclusion: { in: %w[start stop], message: 'should be start or stop' }, + allow_nil: true + + validates :on_stop, type: String, allow_nil: true end end @@ -54,9 +60,17 @@ module Gitlab value[:url] end + def action + value[:action] || 'start' + end + + def on_stop + value[:on_stop] + end + def value case @config - when String then { name: @config } + when String then { name: @config, action: 'start' } when Hash then @config else {} end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 68ea4eeae31..b565586ee14 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -19,10 +19,22 @@ feature 'Environments', feature: true do visit namespace_project_environments_path(project.namespace, project) end + context 'shows two tabs' do + scenario 'shows "Available" and "Stopped" tab with links' do + expect(page).to have_link('Available') + expect(page).to have_link('Stopped') + end + end + context 'without environments' do scenario 'does show no environments' do expect(page).to have_content('You don\'t have any environments right now.') end + + scenario 'does show 0 as counter for environments in both tabs' do + expect(page.find('.js-available-environments-count').text).to eq('0') + expect(page.find('.js-stopped-environments-count').text).to eq('0') + end end context 'with environments' do @@ -32,6 +44,11 @@ feature 'Environments', feature: true do expect(page).to have_link(environment.name) end + scenario 'does show number of available and stopped environments' do + expect(page.find('.js-available-environments-count').text).to eq('1') + expect(page.find('.js-stopped-environments-count').text).to eq('0') + end + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') @@ -44,7 +61,7 @@ feature 'Environments', feature: true do scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end - + scenario 'does show deployment internal id' do expect(page).to have_content(deployment.iid) end @@ -65,20 +82,51 @@ feature 'Environments', feature: true do expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + scenario 'does show build name and id' do expect(page).to have_link("#{build.name} (##{build.id})") end - + + scenario 'does not show stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + + scenario 'does not show external link button' do + expect(page).not_to have_css('external-url') + end + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end + + context 'with stop action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + + scenario 'does show stop button' do + expect(page).to have_selector('.stop-env-link') + end + + scenario 'starts build when stop button clicked' do + first('.stop-env-link').click + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + let(:role) { :reporter } + + scenario 'does not show stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + end + end end end end @@ -127,6 +175,10 @@ feature 'Environments', feature: true do expect(page).to have_link('Re-deploy') end + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } @@ -140,16 +192,39 @@ feature 'Environments', feature: true do expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end + + context 'with stop action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + + scenario 'does show stop button' do + expect(page).to have_link('Stop') + end + + scenario 'does allow to stop environment' do + click_link('Stop') + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + let(:role) { :reporter } + + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end + end + end end end end @@ -196,29 +271,4 @@ feature 'Environments', feature: true do end end end - - describe 'when deleting existing environment' do - given(:environment) { create(:environment, project: project) } - - before do - visit namespace_project_environment_path(project.namespace, project, environment) - end - - context 'when logged as master' do - given(:role) { :master } - - scenario 'does delete environment' do - click_link 'Destroy' - expect(page).not_to have_link(environment.name) - end - end - - context 'when logged as developer' do - given(:role) { :developer } - - scenario 'does not have a Destroy link' do - expect(page).not_to have_link('Destroy') - end - end - end end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index bc2b0ff3e2c..c3c3ab33872 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do expect(page).not_to have_link "Merge When Build Succeeds" end end - + def visit_merge_request(merge_request) visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index 8e23ec50d4a..6676821b807 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -4,23 +4,58 @@ feature 'Widget Deployments Header', feature: true, js: true do include WaitForAjax describe 'when deployed to an environment' do - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do - create(:deployment, environment: environment, sha: project.commit('master').id) - end + given(:user) { create(:user) } + given(:project) { merge_request.target_project } + given(:merge_request) { create(:merge_request, :merged) } + given(:environment) { create(:environment, project: project) } + given(:role) { :developer } + given(:sha) { project.commit('master').id } + given!(:deployment) { create(:deployment, environment: environment, sha: sha) } + given!(:manual) { } - before do - login_as :admin + background do + login_as(user) + project.team << [user, role] visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'displays that the environment is deployed' do + scenario 'displays that the environment is deployed' do wait_for_ajax expect(page).to have_content("Deployed to #{environment.name}") expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) end + + context 'with stop action' do + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) do + create(:deployment, environment: environment, ref: merge_request.target_branch, + sha: sha, deployable: build, on_stop: 'close_app') + end + + background do + wait_for_ajax + end + + scenario 'does show stop button' do + expect(page).to have_link('Stop environment') + end + + scenario 'does start build when stop button clicked' do + click_link('Stop environment') + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + given(:role) { :reporter } + + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop environment') + end + end + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 6dedd25e9d3..84f21631719 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -754,7 +754,7 @@ module Ci it 'does return production' do expect(builds.size).to eq(1) expect(builds.first[:environment]).to eq(environment) - expect(builds.first[:options]).to include(environment: { name: environment }) + expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) end end @@ -796,6 +796,52 @@ module Ci expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") end end + + context 'when on_stop is specified' do + let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } + let(:config) { { review: review, close_review: close_review }.compact } + + context 'with matching job' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } + + it 'does return a list of builds' do + expect(builds.size).to eq(2) + expect(builds.first[:environment]).to eq('review') + end + end + + context 'without matching job' do + let(:close_review) { nil } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review is not defined') + end + end + + context 'with close job without environment' do + let(:close_review) { { stage: 'deploy', script: 'test' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') + end + end + + context 'with close job for different environment' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') + end + end + + context 'with close job without stop action' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') + end + end + end end describe "Dependencies" do diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb index df453223da7..df925ff1afd 100644 --- a/spec/lib/gitlab/ci/config/node/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do describe '#value' do it 'returns valid hash' do - expect(entry.value).to eq(name: 'production') + expect(entry.value).to include(name: 'production') end end @@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do end end + context 'when valid action is used' do + let(:config) do + { name: 'production', + action: 'start' } + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when invalid action is used' do + let(:config) do + { name: 'production', + action: 'invalid' } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about invalid action' do + expect(entry.errors) + .to include 'environment action should be start or stop' + end + end + end + + context 'when on_stop is used' do + let(:config) do + { name: 'production', + on_stop: 'close_app' } + end + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when invalid on_stop is used' do + let(:config) do + { name: 'production', + on_stop: false } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about invalid action' do + expect(entry.errors) + .to include 'environment on stop should be a string' + end + end + end + context 'when variables are used for environment' do let(:config) do { name: 'review/$CI_BUILD_REF_NAME', diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 01a4a53a264..ca594a320c0 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -48,4 +48,50 @@ describe Deployment, models: true do end end end + + describe '#stop_action' do + let(:build) { create(:ci_build) } + + subject { deployment.stop_action } + + context 'when no other actions' do + let(:deployment) { FactoryGirl.build(:deployment, deployable: build) } + + it { is_expected.to be_nil } + end + + context 'with other actions' do + let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + + context 'when matching action is defined' do + let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') } + + it { is_expected.to be_nil } + end + + context 'when no matching action is defined' do + let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } + + it { is_expected.to eq(close_action) } + end + end + end + + describe '#stoppable?' do + subject { deployment.stoppable? } + + context 'when no other actions' do + let(:deployment) { build(:deployment) } + + it { is_expected.to be_falsey } + end + + context 'when matching action is defined' do + let(:build) { create(:ci_build) } + let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } + let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index e172ee8e590..a94e6d0165f 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -8,6 +8,8 @@ describe Environment, models: true do it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } + it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } + it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_within(0..255) } @@ -96,4 +98,72 @@ describe Environment, models: true do is_expected.to be_nil end end + + describe '#stoppable?' do + subject { environment.stoppable? } + + context 'when no other actions' do + it { is_expected.to be_falsey } + end + + context 'when matching action is defined' do + let(:build) { create(:ci_build) } + let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + + context 'when environment is available' do + before do + environment.start + end + + it { is_expected.to be_truthy } + end + + context 'when environment is stopped' do + before do + environment.stop + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#stop!' do + let(:user) { create(:user) } + + subject { environment.stop!(user) } + + before do + expect(environment).to receive(:stoppable?).and_call_original + end + + context 'when no other actions' do + it { is_expected.to be_nil } + end + + context 'when matching action is defined' do + let(:build) { create(:ci_build) } + let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + + context 'when action did not yet finish' do + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } + + it 'returns the same action' do + expect(subject).to eq(close_action) + expect(subject.user).to eq(user) + end + end + + context 'if action did finish' do + let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') } + + it 'returns a new action of the same type' do + is_expected.to be_persisted + expect(subject.name).to eq(close_action.name) + expect(subject.user).to eq(user) + end + end + end + end end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 0b84c7262c3..cf0a18aacec 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do let(:service) { described_class.new(project, user, params) } describe '#execute' do + let(:options) { nil } let(:params) do { environment: 'production', ref: 'master', tag: false, sha: '97de212e80737a608d939f648d959671fb0a0142', + options: options } end @@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do end context 'when environment exist' do - before { create(:environment, project: project, name: 'production') } + let!(:environment) { create(:environment, project: project, name: 'production') } it 'does not create a new environment' do expect { subject }.not_to change { Environment.count } @@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do it 'does create a deployment' do expect(subject).to be_persisted end + + context 'and start action is defined' do + let(:options) { { action: 'start' } } + + context 'and environment is stopped' do + before do + environment.stop + end + + it 'makes environment available' do + subject + + expect(environment.reload).to be_available + end + + it 'does create a deployment' do + expect(subject).to be_persisted + end + end + end + + context 'and stop action is defined' do + let(:options) { { action: 'stop' } } + + context 'and environment is available' do + before do + environment.start + end + + it 'makes environment stopped' do + subject + + expect(environment.reload).to be_stopped + end + + it 'does not create a deployment' do + expect(subject).to be_nil + end + end + end end context 'for environment with invalid name' do @@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do end it 'does not create a deployment' do - expect(subject).not_to be_persisted + expect(subject).to be_nil end end @@ -83,6 +125,25 @@ describe CreateDeploymentService, services: true do it 'does create a new deployment' do expect(subject).to be_persisted end + + context 'and environment exist' do + let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') } + + it 'does not create a new environment' do + expect { subject }.not_to change { Environment.count } + end + + it 'updates external url' do + subject + + expect(subject.environment.name).to eq('review-apps/feature-review-apps') + expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') + end + + it 'does create a new deployment' do + expect(subject).to be_persisted + end + end end context 'when project was removed' do -- GitLab From 406615ae2bc4d6535d382834263c68548a2523d9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 19:01:55 +0000 Subject: [PATCH 011/109] Merge branch 'issue_828' into 'master' Prevent wrong markdown on issue ids when project has Jira service activated fixes gitlab-org/gitlab-ee#828 See merge request !6728 --- CHANGELOG.md | 1 + app/models/external_issue.rb | 5 -- app/models/project.rb | 4 ++ .../project_services/issue_tracker_service.rb | 6 ++ app/models/project_services/jira_service.rb | 5 ++ .../filter/abstract_reference_filter.rb | 16 ++++- .../filter/external_issue_reference_filter.rb | 29 ++++---- lib/banzai/filter/issue_reference_filter.rb | 8 +++ .../external_issue_reference_filter_spec.rb | 72 ++++++++++++++++--- .../filter/issue_reference_filter_spec.rb | 17 +---- spec/models/external_issue_spec.rb | 15 ---- .../project_services/jira_service_spec.rb | 9 +++ .../project_services/redmine_service_spec.rb | 8 +++ spec/services/git_push_service_spec.rb | 60 ++++++++++------ .../merge_requests/merge_service_spec.rb | 12 ++++ .../issue_tracker_service_shared_example.rb | 15 ++++ 16 files changed, 199 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11100044cee..c09957ba8ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Update runner version only when updating contacted_at - Add link from system note to compare with previous version - Use gitlab-shell v3.6.6 + - Ignore references to internal issues when using external issues tracker - Ability to resolve merge request conflicts with editor !6374 - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index b7894c99846..fd9a8c1b8b7 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -29,11 +29,6 @@ class ExternalIssue @project end - # Pattern used to extract `JIRA-123` issue references from text - def self.reference_pattern - @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} - end - def to_reference(_from_project = nil) id end diff --git a/app/models/project.rb b/app/models/project.rb index a6039bb8cc4..653c38322c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -668,6 +668,10 @@ class Project < ActiveRecord::Base end end + def issue_reference_pattern + issues_tracker.reference_pattern + end + def default_issues_tracker? !external_issue_tracker end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index d1df6d0292f..b26ddd518d7 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -3,6 +3,12 @@ class IssueTrackerService < Service default_value_for :category, 'issue_tracker' + # Pattern used to extract links from comments + # Override this method on services that uses different patterns + def reference_pattern + @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?\d+)} + end + def default? default end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 97bcbacf2b9..f81b66fd219 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -13,6 +13,11 @@ class JiraService < IssueTrackerService before_update :reset_password + # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 + def reference_pattern + @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} + end + def reset_password # don't reset the password if a new one is provided if api_url_changed? && !password_touched? diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index affe34394c2..cb213a76a05 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -208,8 +208,12 @@ module Banzai @references_per_project ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } - regex = Regexp.union(object_class.reference_pattern, - object_class.link_reference_pattern) + regex = + if uses_reference_pattern? + Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) + else + object_class.link_reference_pattern + end nodes.each do |node| node.to_html.scan(regex) do @@ -295,6 +299,14 @@ module Banzai value end end + + # There might be special cases like filters + # that should ignore reference pattern + # eg: IssueReferenceFilter when using a external issues tracker + # In those cases this method should be overridden on the filter subclass + def uses_reference_pattern? + true + end end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index eaa702952cc..0d20be557a0 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -8,7 +8,7 @@ module Banzai # Public: Find `JIRA-123` issue references in text # - # ExternalIssueReferenceFilter.references_in(text) do |match, issue| + # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue| # "##{issue}" # end # @@ -17,8 +17,8 @@ module Banzai # Yields the String match and the String issue reference. # # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(ExternalIssue.reference_pattern) do |match| + def self.references_in(text, pattern) + text.gsub(pattern) do |match| yield match, $~[:issue] end end @@ -27,7 +27,7 @@ module Banzai # Early return if the project isn't using an external tracker return doc if project.nil? || default_issues_tracker? - ref_pattern = ExternalIssue.reference_pattern + ref_pattern = issue_reference_pattern ref_start_pattern = /\A#{ref_pattern}\z/ each_node do |node| @@ -60,7 +60,7 @@ module Banzai def issue_link_filter(text, link_text: nil) project = context[:project] - self.class.references_in(text) do |match, id| + self.class.references_in(text, issue_reference_pattern) do |match, id| ExternalIssue.new(id, project) url = url_for_issue(id, project, only_path: context[:only_path]) @@ -82,18 +82,21 @@ module Banzai end def default_issues_tracker? - if RequestStore.active? - default_issues_tracker_cache[project.id] ||= - project.default_issues_tracker? - else - project.default_issues_tracker? - end + external_issues_cached(:default_issues_tracker?) + end + + def issue_reference_pattern + external_issues_cached(:issue_reference_pattern) end private - def default_issues_tracker_cache - RequestStore[:banzai_default_issues_tracker_cache] ||= {} + def external_issues_cached(attribute) + return project.public_send(attribute) unless RequestStore.active? + + cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} } + cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? + cached_attributes[project.id][attribute] end end end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 54c5f9a71a4..4d1bc687696 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -4,6 +4,10 @@ module Banzai # issues that do not exist are ignored. # # This filter supports cross-project references. + # + # When external issues tracker like Jira is activated we should not + # use issue reference pattern, but we should still be able + # to reference issues from other GitLab projects. class IssueReferenceFilter < AbstractReferenceFilter self.reference_type = :issue @@ -11,6 +15,10 @@ module Banzai Issue end + def uses_reference_pattern? + context[:project].default_issues_tracker? + end + def find_object(project, iid) issues_per_project[project][iid] end diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 7116c09fb21..2f9343fadaf 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -7,12 +7,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do IssuesHelper end - let(:project) { create(:jira_project) } - - context 'JIRA issue references' do - let(:issue) { ExternalIssue.new('JIRA-123', project) } - let(:reference) { issue.to_reference } - + shared_examples_for "external issue tracker" do it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end @@ -20,6 +15,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Issue #{reference}" + expect(filter(act).to_html).to eq exp end end @@ -33,25 +29,30 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = filter("Issue #{reference}") + issue_id = doc.css('a').first.attr("data-external-issue") + expect(doc.css('a').first.attr('href')) - .to eq helper.url_for_issue(reference, project) + .to eq helper.url_for_issue(issue_id, project) end it 'links to the external tracker' do doc = filter("Issue #{reference}") + link = doc.css('a').first.attr('href') + issue_id = doc.css('a').first.attr("data-external-issue") - expect(link).to eq "http://jira.example/browse/#{reference}" + expect(link).to eq(helper.url_for_issue(issue_id, project)) end it 'links with adjacent text' do doc = filter("Issue (#{reference}.)") + expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/) end it 'includes a title attribute' do doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" + expect(doc.css('a').first.attr('title')).to include("Issue in #{project.issues_tracker.title}") end it 'escapes the title attribute' do @@ -69,9 +70,60 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'supports an :only_path context' do doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + issue_id = doc.css('a').first["data-external-issue"] + + expect(link).to eq helper.url_for_issue(issue_id, project, only_path: true) + end + + context 'with RequestStore enabled' do + let(:reference_filter) { HTML::Pipeline.new([described_class]) } + + before { allow(RequestStore).to receive(:active?).and_return(true) } + + it 'queries the collection on the first call' do + expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original + expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original + + not_cached = reference_filter.call("look for #{reference}", { project: project }) + + expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) + expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) + + cached = reference_filter.call("look for #{reference}", { project: project }) - expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true) + # Links must be the same + expect(cached[:output].css('a').first[:href]).to eq(not_cached[:output].css('a').first[:href]) + end + end + end + + context "redmine project" do + let(:project) { create(:redmine_project) } + let(:issue) { ExternalIssue.new("#123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "jira project" do + let(:project) { create(:jira_project) } + let(:reference) { issue.to_reference } + + context "with right markdown" do + let(:issue) { ExternalIssue.new("JIRA-123", project) } + + it_behaves_like "external issue tracker" + end + + context "with wrong markdown" do + let(:issue) { ExternalIssue.new("#123", project) } + + it "ignores reference" do + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end end end end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index fce86a9b6ad..a2025672ad9 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -25,9 +25,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do - expect_any_instance_of(described_class).to receive(:find_object). - with(project, issue.iid). - and_return(nil) + allow(project).to receive(:default_issues_tracker?).and_return(false) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -199,19 +197,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - context 'referencing external issues' do - let(:project) { create(:redmine_project) } - - it 'renders internal issue IDs as external issue links' do - doc = reference_filter('#1') - link = doc.css('a').first - - expect(link.attr('data-reference-type')).to eq('external_issue') - expect(link.attr('title')).to eq('Issue in Redmine') - expect(link.attr('data-external-issue')).to eq('1') - end - end - describe '#issues_per_Project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 4fc3b065592..ebba6e14578 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -10,21 +10,6 @@ describe ExternalIssue, models: true do it { is_expected.to include_module(Referable) } end - describe '.reference_pattern' do - it 'allows underscores in the project name' do - expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' - end - - it 'allows numbers in the project name' do - expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' - end - - it 'requires the project name to begin with A-Z' do - expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' - end - end - describe '#to_reference' do it 'returns a String reference to the object' do expect(issue.to_reference).to eq issue.id diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index b48a3176007..6ff32aea018 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -30,6 +30,15 @@ describe JiraService, models: true do end end + describe '#reference_pattern' do + it_behaves_like 'allows project key on reference pattern' + + it 'does not allow # on the code' do + expect(subject.reference_pattern.match('#123')).to be_nil + expect(subject.reference_pattern.match('1#23#12')).to be_nil + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index b8679cd2563..0a7b237a051 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -26,4 +26,12 @@ describe RedmineService, models: true do it { is_expected.not_to validate_presence_of(:new_issue_url) } end end + + describe '#reference_pattern' do + it_behaves_like 'allows project key on reference pattern' + + it 'does allow # on the reference' do + expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') + end + end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 8dda34c7a03..ad5170afc21 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -415,7 +415,7 @@ describe GitPushService, services: true do it "doesn't close issues when external issue tracker is in use" do allow_any_instance_of(Project).to receive(:default_issues_tracker?). and_return(false) - external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid) + external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) # The push still shouldn't create cross-reference notes. @@ -484,30 +484,46 @@ describe GitPushService, services: true do end context "closing an issue" do - let(:message) { "this is some work.\n\ncloses JIRA-1" } - - it "initiates one api call to jira server to close the issue" do - transition_body = { - transition: { - id: '2' - } - }.to_json - - execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_transition_url).with( - body: transition_body - ).once + let(:message) { "this is some work.\n\ncloses JIRA-1" } + let(:transition_body) { { transition: { id: '2' } }.to_json } + let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } + + context "using right markdown" do + it "initiates one api call to jira server to close the issue" do + execute_service(project, commit_author, @oldrev, @newrev, @ref ) + + expect(WebMock).to have_requested(:post, jira_api_transition_url).with( + body: transition_body + ).once + end + + it "initiates one api call to jira server to comment on the issue" do + execute_service(project, commit_author, @oldrev, @newrev, @ref ) + + expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + body: comment_body + ).once + end end - it "initiates one api call to jira server to comment on the issue" do - comment_body = { - body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." - }.to_json + context "using wrong markdown" do + let(:message) { "this is some work.\n\ncloses #1" } - execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_comment_url).with( - body: comment_body - ).once + it "does not initiates one api call to jira server to close the issue" do + execute_service(project, commit_author, @oldrev, @newrev, @ref ) + + expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with( + body: transition_body + ) + end + + it "does not initiates one api call to jira server to comment on the issue" do + execute_service(project, commit_author, @oldrev, @newrev, @ref ) + + expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with( + body: comment_body + ).once + end end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index ee53e110aee..47d5536818e 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -74,6 +74,18 @@ describe MergeRequests::MergeService, services: true do service.execute(merge_request) end + + context "wrong issue markdown" do + it 'does not close issues on JIRA issue tracker' do + jira_issue = ExternalIssue.new('#123', project) + commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") + allow(merge_request).to receive(:commits).and_return([commit]) + + expect_any_instance_of(JiraService).not_to receive(:close_issue) + + service.execute(merge_request) + end + end end end diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb index b6d7436c360..e70b3963d9d 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -5,3 +5,18 @@ RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr| it { is_expected.not_to allow_value('ftp://example.com').for(url_attr) } it { is_expected.not_to allow_value('herp-and-derp').for(url_attr) } end + +RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| + it 'allows underscores in the project name' do + expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + + it 'allows numbers in the project name' do + expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + end + + it 'requires the project name to begin with A-Z' do + expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end +end -- GitLab From c2f0ea7d51c51fbb51c54dbb706f27ed34d578c9 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 18 Oct 2016 19:38:01 +0000 Subject: [PATCH 012/109] Merge branch '19991-triggered-pipeline' into 'master' Triggered pipelines #### What does this MR do? Separates trigger into its own column #### Screenshots (if relevant) ![Screen_Shot_2016-10-07_at_4.21.54_PM](/uploads/092e8205d329b66b34045fe17c5e6e4f/Screen_Shot_2016-10-07_at_4.21.54_PM.png) ![Screen_Shot_2016-10-17_at_9.13.10_AM](/uploads/7df90e0e2a07a9f282df3605787d3cc2/Screen_Shot_2016-10-17_at_9.13.10_AM.png) ![Screen_Shot_2016-10-17_at_9.15.07_AM](/uploads/b7dc0307c8549e72c3f812c3cd91833a/Screen_Shot_2016-10-17_at_9.15.07_AM.png) #### What are the relevant issue numbers? Closes #19991 See merge request !6753 --- app/assets/stylesheets/pages/pipelines.scss | 39 ++++++++------- app/views/projects/ci/builds/_build.html.haml | 2 +- .../projects/ci/pipelines/_pipeline.html.haml | 47 +++++++++++-------- .../projects/commit/_pipelines_list.haml | 1 + .../projects/deployments/_commit.html.haml | 2 +- app/views/projects/pipelines/index.html.haml | 11 +++-- 6 files changed, 58 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1dc55538f65..bd9d8da75e4 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -23,10 +23,17 @@ .table.ci-table { min-width: 1200px; - .branch-commit { - width: 33%; + .pipeline-id { + color: $black; } + .branch-commit { + width: 30%; + + .branch-name { + width: 195px + } + } } } @@ -92,6 +99,15 @@ } } + .avatar { + margin-left: 0; + float: none; + } + + .api { + color: $code-color; + } + .branch-commit { .branch-name { @@ -119,7 +135,6 @@ .commit-id { color: $gl-link-color; - margin-right: 8px; } .commit-title { @@ -130,10 +145,6 @@ text-overflow: ellipsis; } - .avatar { - margin-left: 0; - } - .label { margin-right: 4px; } @@ -149,17 +160,11 @@ .icon-container { display: inline-block; - text-align: right; - width: 15px; - - .fa { - position: relative; - right: 3px; - } + width: 10px; - svg { - position: relative; - right: 1px; + &.commit-icon { + width: 15px; + text-align: center; } } diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index bf157e4f64a..94632056b15 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -27,7 +27,7 @@ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - .icon-container + .icon-container.commit-icon = custom_icon("icon_commit") - if commit_sha diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 36eadbd2bf1..c6f359f5679 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -9,17 +9,15 @@ = ci_icon_for_status(status) - else = ci_status_with_icon(status) - %td.branch-commit + + %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span ##{pipeline.id} - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" + %span.pipeline-id ##{pipeline.id} + %span by + - if pipeline.user + = user_avatar(user: pipeline.user, size: 20) + - else + %span.api.monospace API - if pipeline.latest? %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - if pipeline.triggered? @@ -29,6 +27,16 @@ - if pipeline.builds.any?(&:stuck?) %span.label.label-warning stuck + %td.branch-commit + - if pipeline.ref && show_branch + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" + - if show_commit + .icon-container.commit-icon + = custom_icon("icon_commit") + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" + %p.commit-title - if commit = pipeline.commit = author_avatar(commit, size: 20) @@ -36,16 +44,15 @@ - else Cant find HEAD commit for this branch - - - stages_status = pipeline.statuses.relevant.latest.stages_status - %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" - - if status - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) + - stages_status = pipeline.statuses.relevant.latest.stages_status + %td.stage-cell + - stages.each do |stage| + - status = stages_status[stage] + - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - if status + .stage-container + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = ci_icon_for_status(status) %td - if pipeline.duration diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 640651e93f5..ac451441eec 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -8,6 +8,7 @@ %tbody %th Status %th Pipeline + %th Commit %th Stages %th %th diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 28813babd7b..ff250eeca50 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -3,7 +3,7 @@ .icon-container = deployment.tag? ? icon('tag') : icon('code-fork') = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name" - .icon-container + .icon-container.commit-icon = custom_icon("icon_commit") = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 9eeef5f57b4..4bc49072f35 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -45,11 +45,12 @@ .table-holder %table.table.ci-table %thead - %th.col-xs-1.col-sm-1 Status - %th.col-xs-2.col-sm-4 Pipeline - %th.col-xs-2.col-sm-2 Stages - %th.col-xs-2.col-sm-2 - %th.hidden-xs.col-sm-3 + %th Status + %th Pipeline + %th Commit + %th Stages + %th + %th.hidden-xs = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages = paginate @pipelines, theme: 'gitlab' -- GitLab From d2aa46b71e7060524ed47d9a1b9fbb7c6ec0127b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 18 Oct 2016 20:49:51 +0000 Subject: [PATCH 013/109] Merge branch '22782-external-link-filter-with-non-lowercase-scheme' into 'master' Add Nofollow for uppercased external url protocols Closes #22782 See merge request !6820 --- CHANGELOG.md | 1 + lib/banzai/filter/external_link_filter.rb | 34 ++++++++++++++++--- .../filter/external_link_filter_spec.rb | 34 +++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c09957ba8ed..a816e30a9c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Retouch environments list and deployments list - Add multiple command support for all label related slash commands !6780 (barthc) - Add Container Registry on/off status to Admin Area !6638 (the-undefined) + - Add Nofollow for uppercased scheme in external urls !6820 (the-undefined) - Allow empty merge requests !6384 (Artem Sidorenko) - Grouped pipeline dropdown is a scrollable container - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 0a29c547a4d..2f19b59e725 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -3,10 +3,17 @@ module Banzai # HTML Filter to modify the attributes of external links class ExternalLinkFilter < HTML::Pipeline::Filter def call - # Skip non-HTTP(S) links and internal links - doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node| - node.set_attribute('rel', 'nofollow noreferrer') - node.set_attribute('target', '_blank') + links.each do |node| + href = href_to_lowercase_scheme(node["href"].to_s) + + unless node["href"].to_s == href + node.set_attribute('href', href) + end + + if href =~ /\Ahttp(s)?:\/\// && external_url?(href) + node.set_attribute('rel', 'nofollow noreferrer') + node.set_attribute('target', '_blank') + end end doc @@ -14,6 +21,25 @@ module Banzai private + def links + query = 'descendant-or-self::a[@href and not(@href = "")]' + doc.xpath(query) + end + + def href_to_lowercase_scheme(href) + scheme_match = href.match(/\A(\w+):\/\//) + + if scheme_match + scheme_match.to_s.downcase + scheme_match.post_match + else + href + end + end + + def external_url?(url) + !url.start_with?(internal_url) + end + def internal_url @internal_url ||= Gitlab.config.gitlab.url end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 695a5bc6fd4..167397c736b 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -46,4 +46,38 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(doc.at_css('a')['rel']).to include 'noreferrer' end end + + context 'for non-lowercase scheme links' do + let(:doc_with_http) { filter %q(

Google

) } + let(:doc_with_https) { filter %q(

Google

) } + + it 'adds rel="nofollow" to external links' do + expect(doc_with_http.at_css('a')).to have_attribute('rel') + expect(doc_with_https.at_css('a')).to have_attribute('rel') + + expect(doc_with_http.at_css('a')['rel']).to include 'nofollow' + expect(doc_with_https.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + expect(doc_with_http.at_css('a')).to have_attribute('rel') + expect(doc_with_https.at_css('a')).to have_attribute('rel') + + expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer' + expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer' + end + + it 'skips internal links' do + internal_link = Gitlab.config.gitlab.url + "/sign_in" + url = internal_link.gsub(/\Ahttp/, 'HtTp') + act = %Q(Login) + exp = %Q(Login) + expect(filter(act).to_html).to eq(exp) + end + + it 'skips relative links' do + exp = act = %q(Relative URL) + expect(filter(act).to_html).to eq(exp) + end + end end -- GitLab From 3fdd00afc73e05ad413be18d9cdbbe2e3c74b465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 19 Oct 2016 10:34:31 +0000 Subject: [PATCH 014/109] Merge branch 'fix-system-hook-api' into 'master' API: Fix Sytem hooks delete behavior ## What does this MR do? This corrects the delete API for system hooks. Returning 200 is not the right way indicating a hooks is not found. ## What are the relevant issue numbers? Discussed in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6861/diffs#609af00c90e3d5241064d1404e3e018a3235634a_64_62 See merge request !6883 --- doc/api/system_hooks.md | 7 ++----- lib/api/system_hooks.rb | 10 ++++------ spec/requests/api/system_hooks_spec.rb | 7 ++++--- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 1802fae14fe..073e99b7147 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -98,11 +98,8 @@ Example response: ## Delete system hook -Deletes a system hook. This is an idempotent API function and returns `200 OK` -even if the hook is not available. - -If the hook is deleted, a JSON object is returned. An error is raised if the -hook is not found. +Deletes a system hook. It returns `200 OK` if the hooks is deleted and +`404 Not Found` if the hook is not found. --- diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 2e76b91051f..794e34874f4 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -56,12 +56,10 @@ module API requires :id, type: Integer, desc: 'The ID of the system hook' end delete ":id" do - begin - hook = SystemHook.find(params[:id]) - present hook.destroy, with: Entities::Hook - rescue - # SystemHook raises an Error if no hook with id found - end + hook = SystemHook.find_by(id: params[:id]) + not_found!('System hook') unless hook + + present hook.destroy, with: Entities::Hook end end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 1ce2658569e..f8a1aed5441 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -73,9 +73,10 @@ describe API::API, api: true do end.to change { SystemHook.count }.by(-1) end - it "returns success if hook id not found" do - delete api("/hooks/12345", admin) - expect(response).to have_http_status(200) + it 'returns 404 if the system hook does not exist' do + delete api('/hooks/12345', admin) + + expect(response).to have_http_status(404) end end end -- GitLab From 428bc2a5ae56ac3f61c458352b1a01c0687cf08e Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 20 Oct 2016 08:07:40 +0000 Subject: [PATCH 015/109] Merge branch '23346-pipeline-buttons-cutoff' into 'master' Smaller min-width for MR pipeline table ## What does this MR do? Nests class under `.tab-pane` to make sure buttons don't get cut off ## Screenshots (if relevant) ![Screen_Shot_2016-10-14_at_11.00.02_AM](/uploads/6e19e0375721f2cb58223c2e7ed89308/Screen_Shot_2016-10-14_at_11.00.02_AM.png) ## What are the relevant issue numbers? Closes #23346 See merge request !6893 --- app/assets/stylesheets/pages/pipelines.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bd9d8da75e4..46ce040d459 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -650,6 +650,10 @@ &.pipelines { + .ci-table { + min-width: 900px; + } + .content-list.pipelines { overflow: auto; } -- GitLab From 09fc48ac19580e862f506db7a4d8ba0c327d3567 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 20 Oct 2016 10:57:33 +0000 Subject: [PATCH 016/109] Merge branch 'issue_22944' into 'master' Create project feature when project is created closes #22944 See merge request !6908 --- CHANGELOG.md | 1 + app/models/project.rb | 7 +---- ...5_generate_project_feature_for_projects.rb | 28 +++++++++++++++++++ db/schema.rb | 2 +- spec/models/project_spec.rb | 14 ++++++++-- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20161019213545_generate_project_feature_for_projects.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a816e30a9c7..08cc9e19bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Cancelled pipelines could be retried. !6927 - Updating verbiage on git basics to be more intuitive + - Fix project_feature record not generated on project creation - Clarify documentation for Runners API (Gennady Trafimenkov) - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username diff --git a/app/models/project.rb b/app/models/project.rb index 653c38322c5..6685baab699 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -32,8 +32,8 @@ class Project < ActiveRecord::Base default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist + after_create :create_project_feature, unless: :project_feature after_save :ensure_dir_exist, if: :namespace_id_changed? - after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -1310,11 +1310,6 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" end - # Prevents the creation of project_feature record for every project - def setup_project_feature - build_project_feature unless project_feature - end - def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb new file mode 100644 index 00000000000..4554e14b0df --- /dev/null +++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb @@ -0,0 +1,28 @@ +class GenerateProjectFeatureForProjects < ActiveRecord::Migration + DOWNTIME = true + + DOWNTIME_REASON = <<-HEREDOC + Application was eager loading project_feature for all projects generating an extra query + everytime a project was fetched. We removed that behavior to avoid the extra query, this migration + makes sure all projects have a project_feature record associated. + HEREDOC + + def up + # Generate enabled values for each project feature 20, 20, 20, 20, 20 + # All features are enabled by default + enabled_values = [ProjectFeature::ENABLED] * 5 + + execute <<-EOF.strip_heredoc + INSERT INTO project_features + (project_id, merge_requests_access_level, builds_access_level, + issues_access_level, snippets_access_level, wiki_access_level) + (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features + ON project_features.project_id = projects.id + WHERE project_features.id IS NULL) + EOF + end + + def down + "Not needed" + end +end diff --git a/db/schema.rb b/db/schema.rb index 65f55aa109b..a3c7fc2fd57 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161018024550) do +ActiveRecord::Schema.define(version: 20161019213545) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e6d98e25d0b..f4dda1ee558 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,6 +67,14 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } + context 'after create' do + it "creates project feature" do + project = FactoryGirl.build(:project) + + expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + end + end + describe '#members & #requesters' do let(:project) { create(:project, :public) } let(:requester) { create(:user) } @@ -531,9 +539,9 @@ describe Project, models: true do end describe '#has_wiki?' do - let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } - let(:wiki_enabled_project) { build(:project) } - let(:external_wiki_project) { build(:project, has_external_wiki: true) } + let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) } + let(:wiki_enabled_project) { create(:project) } + let(:external_wiki_project) { create(:project, has_external_wiki: true) } it 'returns true if project is wiki enabled or has external wiki' do expect(wiki_enabled_project).to have_wiki -- GitLab From 7bd4c2d6e728ccf37e16c6283224d4537a5ba342 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 20 Oct 2016 12:59:35 +0000 Subject: [PATCH 017/109] Merge branch 'unrevert-tab-order-MR' into 'master' Change input order on Sign In form for better tabbing. ## What does this MR do? This unreverts 8751491b, which was mistakenly reverted in !6328. It also changes the implementation of the original commit to work with the new login styling and markup. cc: @ClemMakesApps ## Screenshots (if relevant) (Disregard the grey borders on invalid inputs -- for some reason, Gifox isn't picking up some colors for me :shrug: ) ![2016-10-17_11.32.05](/uploads/f395b2d77fb47cac3f9d4fa1bc3d7d71/2016-10-17_11.32.05.gif) ## Does this MR meet the acceptance criteria? - [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - Tests - [x] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6867 See merge request !6928 --- app/assets/stylesheets/pages/login.scss | 17 +++++++++++++++++ app/views/devise/sessions/_new_base.html.haml | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index e6d9be5185d..bdb13bee178 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -53,6 +53,7 @@ margin: 0 0 10px; } + .login-footer { margin-top: 10px; @@ -246,3 +247,19 @@ padding: 65px; // height of footer + bottom padding of email confirmation link } } + +// For sign in pane only, to improve tab order, the following removes the submit button from +// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928 + +.login-box { + .new_user { + position: relative; + padding-bottom: 35px; + } + + .move-submit-down { + position: absolute; + width: 100%; + bottom: 0; + } +} diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index a96b579c593..525e7d99d71 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -5,6 +5,8 @@ %div.form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." + %div.submit-container.move-submit-down + = f.submit "Sign in", class: "btn btn-save" - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} @@ -12,5 +14,3 @@ %span Remember me .pull-right = link_to "Forgot your password?", new_password_path(resource_name) - %div.submit-container - = f.submit "Sign in", class: "btn btn-save" -- GitLab From 38e4d464fbafd02e8db688487502c1b7033b2373 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 18 Oct 2016 17:39:26 +0000 Subject: [PATCH 018/109] Merge branch '23311-fix-double-escaping' into 'master' Stop event_commit_title from escaping its output Fixes a double-escape issue (actually triple-escape!) viewing the activity feed Closes #23311 Related to #23258 !6832 See merge request !6930 --- app/helpers/events_helper.rb | 2 +- spec/helpers/events_helper_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index bfedcb1c42b..f8ded05c31a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -154,7 +154,7 @@ module EventsHelper end def event_commit_title(message) - escape_once(truncate(message.split("\n").first, length: 70)) + (message.split("\n").first || "").truncate(70) rescue "--broken encoding" end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 022aba0c0d0..594b40303bc 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -62,4 +62,21 @@ describe EventsHelper do expect(helper.event_note(input)).to eq(expected) end end + + describe '#event_commit_title' do + let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 } + subject { helper.event_commit_title(message) } + + it "returns the first line, truncated to 70 chars" do + is_expected.to eq(message[0..66] + "...") + end + + it "is not html-safe" do + is_expected.not_to be_a(ActiveSupport::SafeBuffer) + end + + it "handles empty strings" do + expect(helper.event_commit_title("")).to eq("") + end + end end -- GitLab From 9adc3f4ca3ceee01f6abf6ec6ea29a02d045244c Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 19 Oct 2016 13:56:08 +0000 Subject: [PATCH 019/109] Merge branch 'fix-escaping' into 'master' fix: commit messages being double-escaped in activities tab See merge request !6937 --- CHANGELOG.md | 1 + lib/banzai/filter/html_entity_filter.rb | 2 +- spec/lib/banzai/filter/html_entity_filter_spec.rb | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08cc9e19bb2..8ab13ccd13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) - Fixes padding in all clipboard icons that have .btn class - Fix a typo in doc/api/labels.md + - Fix double-escaping in activities tab (Alexandre Maia) - API: all unknown routing will be handled with 404 Not Found - Add docs for request profiling - Delete dynamic environments diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb index e008fd428b0..f3bd587c28b 100644 --- a/lib/banzai/filter/html_entity_filter.rb +++ b/lib/banzai/filter/html_entity_filter.rb @@ -5,7 +5,7 @@ module Banzai # Text filter that escapes these HTML entities: & " < > class HtmlEntityFilter < HTML::Pipeline::TextFilter def call - ERB::Util.html_escape(text) + ERB::Util.html_escape_once(text) end end end diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb index 4c68ce6d6e4..f9e6bd609f0 100644 --- a/spec/lib/banzai/filter/html_entity_filter_spec.rb +++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb @@ -11,4 +11,9 @@ describe Banzai::Filter::HtmlEntityFilter, lib: true do expect(output).to eq(escaped) end + + it 'does not double-escape' do + escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'") + expect(filter(escaped)).to eq(escaped) + end end -- GitLab From 33ee90b4efd64cebe1d3919d553cf15f64b4ca56 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 18 Oct 2016 17:53:39 +0000 Subject: [PATCH 020/109] Merge branch 'dz-spinach-wait-ajax' into 'master' Wait for ajax for every merge request spinach test ## What does this MR do? * removes duplicate `WaitForAjax` module * ensure we run `wait_for_ajax`after each MR spinach tests ## Why was this MR needed? Because when visit MR page we do ajax call to check CI status. When testing this page with spinach and JS driver we got random failing tests. It happens because of race condition db cleaner drop data before ajax call finished. So we make sure that every MR spinach scenario with js driver waits for ajax before running next scenario ## What are the relevant issue numbers? Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/23461 See merge request !6959 --- features/steps/project/merge_requests.rb | 4 ++++ features/steps/shared/note.rb | 2 -- features/support/env.rb | 2 +- features/support/wait_for_ajax.rb | 11 ----------- spec/support/wait_for_ajax.rb | 4 ++++ 5 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 features/support/wait_for_ajax.rb diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 44346d99f44..2ccab4334eb 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -9,6 +9,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps include SharedUser include WaitForAjax + after do + wait_for_ajax if javascript_test? + end + step 'I click link "New Merge Request"' do click_link "New Merge Request" end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index d3b5b0bdebe..9dc1fc41b3b 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -1,5 +1,3 @@ -require Rails.root.join('features/support/wait_for_ajax') - module SharedNote include Spinach::DSL include WaitForAjax diff --git a/features/support/env.rb b/features/support/env.rb index 569fd444e86..8dbe3624410 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -15,7 +15,7 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers).each do |f| +%w(select2_helper test_env repo_helpers wait_for_ajax).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb deleted file mode 100644 index b90fc112671..00000000000 --- a/features/support/wait_for_ajax.rb +++ /dev/null @@ -1,11 +0,0 @@ -module WaitForAjax - def wait_for_ajax - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? - end -end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb index b90fc112671..0f9dc2dee75 100644 --- a/spec/support/wait_for_ajax.rb +++ b/spec/support/wait_for_ajax.rb @@ -8,4 +8,8 @@ module WaitForAjax def finished_all_ajax_requests? page.evaluate_script('jQuery.active').zero? end + + def javascript_test? + [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver) + end end -- GitLab From 1a2ca1686fbf03c6f14efe24e3316aeea8d03b10 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 20 Oct 2016 08:50:27 +0000 Subject: [PATCH 021/109] Merge branch 'boards-blank-state-size-fix' into 'master' Fixed height of issue board blank state ## What does this MR do? The height of the issue board blank state was causing some weird scrolling issues. This MR changes the height of the blank state. See merge request !6960 --- app/assets/stylesheets/pages/boards.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 6e81c12aa55..d8fabbdcebe 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,4 +1,3 @@ -lex [v-cloak] { display: none; } @@ -132,7 +131,7 @@ lex } .board-blank-state { - height: 100%; + height: calc(100% - 49px); padding: $gl-padding; background-color: #fff; } -- GitLab From f0f78bb0b336084e879c1440e9de5ae3de75c19b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 09:37:58 +0000 Subject: [PATCH 022/109] Merge branch 'backport-git-access-spec-changes' into 'master' Backport git access spec changes from EE https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/645#note_16391185 See merge request !6961 --- spec/lib/gitlab/git_access_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index de68e32e5b4..a5aa387f4f7 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -185,6 +185,7 @@ describe Gitlab::GitAccess, lib: true do end end + # Run permission checks for a user def self.run_permission_checks(permissions_matrix) permissions_matrix.keys.each do |role| describe "#{role} access" do @@ -194,13 +195,12 @@ describe Gitlab::GitAccess, lib: true do else project.team << [user, role] end - end - - permissions_matrix[role].each do |action, allowed| - context action do - subject { access.push_access_check(changes[action]) } - it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } + permissions_matrix[role].each do |action, allowed| + context action do + subject { access.push_access_check(changes[action]) } + it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } + end end end end -- GitLab From b06636ab8636307a5fe7fe0771b088352ea68286 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 19 Oct 2016 21:21:44 +0000 Subject: [PATCH 023/109] Merge branch 'ios-tooltips' into 'master' Set webkit-overflow-scrolling to auto for children of body. ## What does this MR do? Fixes weird tooltip layering behavior in iOS Safari. See screenshots and/or the original issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/22816 ## Why was this MR needed? Tooltips were cutoff in Safari. ## Screenshots (if relevant) Before: ![Screen_Shot_2016-10-18_at_7.13.11_PM](/uploads/5558d60b7369a9355f18d34dcc2c179e/Screen_Shot_2016-10-18_at_7.13.11_PM.png) After: ![Screen_Shot_2016-10-18_at_7.13.40_PM](/uploads/ae07002f2f396f135b3078538d5c4ad6/Screen_Shot_2016-10-18_at_7.13.40_PM.png) Also, as part of this fix, I removed applications of `-webkit-overflow-scrolling: auto` in two other places where they're no longer needed. One was the file-holder. I made sure I could reproduce the behavior this was intended to fix, and then made sure this MR still fixed it. Here's the errant behavior: ![2016-10-18_19.00.22](/uploads/0bd3ee3bab44769dfce80c7edaac3248/2016-10-18_19.00.22.gif) Here's what it looks like with this MR: ![2016-10-18_19.00.49](/uploads/e405ded5acdbbbe5e577222c11198691/2016-10-18_19.00.49.gif) ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? See merge request !6962 --- CHANGELOG.md | 1 + app/assets/stylesheets/framework/files.scss | 1 - app/assets/stylesheets/framework/layout.scss | 12 ++++++++++++ app/assets/stylesheets/pages/diff.scss | 1 - 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab13ccd13d..1a23ec38aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,7 @@ Please view this file on the master branch, on stable branches it's out of date. - API: all unknown routing will be handled with 404 Not Found - Add docs for request profiling - Delete dynamic environments + - Fix buggy iOS tooltip layering behavior. - Make guests unable to view MRs on private projects ## 8.12.7 diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 13c1bbf0359..f49d7b92a00 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -167,7 +167,6 @@ */ &.code { padding: 0; - -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 } } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 8bb047db2dd..7baa4296abf 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -27,3 +27,15 @@ body { .container-limited { max-width: $fixed-layout-width; } + + +/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch, +which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side +effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children +of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */ + +.navbar, +.page-gutter, +.page-with-sidebar { + -webkit-overflow-scrolling: auto; +} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index bdc82a8f0f5..fe6421f8b3f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -52,7 +52,6 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; - -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; -- GitLab From 578114dafd8b4ebdb6281b961a1fb4c781b034bc Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 18 Oct 2016 19:24:57 +0000 Subject: [PATCH 024/109] Merge branch 'fix-cycle-analytics' into 'master' Add missing Vue dependency Cycle Analytics is broken because vueJS is not included. This feature will be improved on its second iteration, and also tests will be aded so this won't happen again. See merge request !6965 --- app/assets/javascripts/cycle_analytics.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index cd9886ba58d..bd9accacb8c 100644 --- a/app/assets/javascripts/cycle_analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 @@ -1,3 +1,5 @@ +//= require vue + ((global) => { const COOKIE_NAME = 'cycle_analytics_help_dismissed'; -- GitLab From eb4d7d6c4ed58073cd1401f5121d02162793bade Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 19 Oct 2016 09:55:29 +0000 Subject: [PATCH 025/109] Merge branch 'sh-fix-broken-note-award-emoji' into 'master' Fix broken award emoji for comments Closes #23506 See merge request !6967 --- app/helpers/award_emoji_helper.rb | 2 +- spec/features/issues/award_emoji_spec.rb | 46 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 493f14f6f9d..592ffe7b89f 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -4,7 +4,7 @@ module AwardEmojiHelper 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 (6.5x) - toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace_id, project_id: @project.id, id: awardable.id) + toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace, project_id: @project, id: awardable.id) else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 79cc50bc18e..ef00f209998 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'Awards Emoji', feature: true do + include WaitForAjax + let!(:project) { create(:project) } let!(:user) { create(:user) } @@ -16,20 +18,22 @@ describe 'Awards Emoji', feature: true do project: project) end + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } + before do visit namespace_project_issue_path(project.namespace, project, issue) end it 'increments the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click - sleep 2 + wait_for_ajax expect(thumbsdown_emoji).to have_text("1") end context 'click the thumbsup emoji' do it 'increments the thumbsup emoji', js: true do find('[data-emoji="thumbsup"]').click - sleep 2 + wait_for_ajax expect(thumbsup_emoji).to have_text("1") end @@ -41,7 +45,7 @@ describe 'Awards Emoji', feature: true do context 'click the thumbsdown emoji' do it 'increments the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click - sleep 2 + wait_for_ajax expect(thumbsdown_emoji).to have_text("1") end @@ -49,13 +53,45 @@ describe 'Awards Emoji', feature: true do expect(thumbsup_emoji).to have_text("0") end end + + it 'toggles the smiley emoji on a note', js: true do + toggle_smiley_emoji(true) + + within('.note-awards') do + expect(find(emoji_counter)).to have_text("1") + end + + toggle_smiley_emoji(false) + + within('.note-awards') do + expect(page).not_to have_selector(emoji_counter) + end + end end def thumbsup_emoji - page.all('span.js-counter').first + page.all(emoji_counter).first end def thumbsdown_emoji - page.all('span.js-counter').last + page.all(emoji_counter).last + end + + def emoji_counter + 'span.js-counter' + end + + def toggle_smiley_emoji(status) + within('.note') do + find('.note-emoji-button').click + end + + unless status + first('[data-emoji="smiley"]').click + else + find('[data-emoji="smiley"]').click + end + + wait_for_ajax end end -- GitLab From cc305a01df382ffb3ca62257a0c59b5808993485 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 18 Oct 2016 23:19:51 +0000 Subject: [PATCH 026/109] Merge branch 'fix-scss-lint-pipelines' into 'master' Fix missing semicolon to make scss-lint happy Builds were failing: https://gitlab.com/gitlab-org/gitlab-ce/builds/5256491 See merge request !6969 --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 46ce040d459..5c98a819cc1 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -31,7 +31,7 @@ width: 30%; .branch-name { - width: 195px + width: 195px; } } } -- GitLab From 18bb73be245eeac4dd7c2d34e721887212a18a53 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 19 Oct 2016 00:45:44 +0000 Subject: [PATCH 027/109] Merge branch 'pipeline-visiual-updates' into 'master' Fix pipeline alignment Before: After: See merge request !6971 --- app/assets/stylesheets/pages/pipelines.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 5c98a819cc1..5b8dc7f8c40 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -31,7 +31,7 @@ width: 30%; .branch-name { - width: 195px; + max-width: 195px; } } } @@ -130,7 +130,6 @@ .fa { font-size: 12px; color: $gl-text-color; - margin-left: 5px; } .commit-id { -- GitLab From 9c783a60ba6cbfd8525948e0016b37ce0f0448f1 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 19 Oct 2016 21:18:19 +0000 Subject: [PATCH 028/109] Merge branch '23375-filtering-on-any-label-or-no-label-does-not-work' into 'master' Fix 'No label' and 'Any label' filters ## What does this MR do? Returns the `title`as the `id` for `No label`. ## Are there points in the code the reviewer needs to double check? ## Why was this MR needed? Label filters not working as expected ## Screenshots (if relevant) ![2016-10-19_04.58.08](/uploads/3b079bf28299c1155e0243171ac008f6/2016-10-19_04.58.08.gif) ## Does this MR meet the acceptance criteria? - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [ ] Added for this feature/bug - [ ] All builds are passing - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Branch has no merge conflicts with `master` (if it does - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #23375 See merge request !6974 --- app/assets/javascripts/labels_select.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f1e719937c7..b4f6e70f694 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -266,7 +266,7 @@ }, fieldName: $dropdown.data('field-name'), id: function(label) { - if (label.id <= 0) return; + if (label.id <= 0) return label.title; if ($dropdown.hasClass('js-issuable-form-dropdown')) { return label.id; -- GitLab From 227a9b44cf0dffcdbbf0d65ccfb96818870afc2c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 19 Oct 2016 10:07:56 +0000 Subject: [PATCH 029/109] Merge branch 'sh-improve-merge-service-logging' into 'master' Improve error logging of MergeService Relates to #23505 See merge request !6975 --- app/services/merge_requests/merge_service.rb | 20 +++++++++++++++---- .../merge_requests/merge_service_spec.rb | 4 ++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index b037780c431..ab9056a3250 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -11,14 +11,14 @@ module MergeRequests def execute(merge_request) @merge_request = merge_request - return error('Merge request is not mergeable') unless @merge_request.mergeable? + return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? merge_request.in_locked_state do if commit after_merge success else - error('Can not merge changes') + log_merge_error('Can not merge changes', true) end end end @@ -46,8 +46,8 @@ module MergeRequests merge_request.update(merge_error: e.message) false rescue StandardError => e - merge_request.update(merge_error: "Something went wrong during merge") - Rails.logger.error(e.message) + merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") + log_merge_error(e.message) false ensure merge_request.update(in_progress_merge_commit_sha: nil) @@ -65,5 +65,17 @@ module MergeRequests def branch_deletion_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user end + + def log_merge_error(message, http_error = false) + Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") + + error(message) if http_error + end + + def merge_request_info + project = merge_request.project + + "#{project.to_reference}#{merge_request.to_reference}" + end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 47d5536818e..f93d7732a9a 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -132,13 +132,13 @@ describe MergeRequests::MergeService, services: true do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } it 'saves error if there is an exception' do - allow(service).to receive(:repository).and_raise("error") + allow(service).to receive(:repository).and_raise("error message") allow(service).to receive(:execute_hooks) service.execute(merge_request) - expect(merge_request.merge_error).to eq("Something went wrong during merge") + expect(merge_request.merge_error).to eq("Something went wrong during merge: error message") end it 'saves error if there is an PreReceiveError exception' do -- GitLab From 52640a15715c9beae83dbb5cde534547c41ebdb9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 19 Oct 2016 15:49:44 +0000 Subject: [PATCH 030/109] Merge branch 'fix/fix-public-builds-name-in-pipeline-settings' into 'master' Fix name of feature that restricts access to builds traces ## What does this MR do? This MR fixes the name of the feature that makes it possible to restrict build traces. This does not disable access to pipelines. Pipelines are still available so we should tie the name with builds, instead of using work "pipeline" there. See merge request !6980 --- app/views/projects/pipelines_settings/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 0740e9b56ab..bebf0ccd54d 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -64,8 +64,8 @@ .checkbox = f.label :public_builds do = f.check_box :public_builds - %strong Public pipelines - .help-block Allow everyone to access pipelines for Public and Internal projects + %strong Public builds + .help-block Allow everyone to access builds traces for Public and Internal projects .form-group.append-bottom-default = f.label :runners_token, "Runners token", class: 'label-light' -- GitLab From aba801995dc510ed09820fd302e5eb6ded058792 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 13:03:13 +0000 Subject: [PATCH 031/109] Merge branch 'rs-revert-title-change' into 'master' Revert issuable title reordering Reverts !6503, and uses `to_reference` rather than `iid` for `Issues#show` and `Issues#edit` titles. See merge request !6983 --- CHANGELOG.md | 1 - app/views/projects/issues/edit.html.haml | 2 +- app/views/projects/issues/show.html.haml | 2 +- app/views/projects/merge_requests/_show.html.haml | 2 +- app/views/projects/merge_requests/edit.html.haml | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a23ec38aa0..9b680499bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,6 @@ Please view this file on the master branch, on stable branches it's out of date. - Optimize GitHub importing for speed and memory - API: expose pipeline data in builds API (!6502, Guilherme Salazar) - Notify the Merger about merge after successful build (Dimitris Karakasilis) - - Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein) - Reduce queries needed to find users using their SSH keys when pushing commits - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - Fix broken repository 500 errors in project list diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 3a6fbbc7fbc..1b7d878c38c 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues" +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 09347ad5fff..6f3f238a436 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@issue.to_reference} #{@issue.title}", "Issues" +- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index fb4afe3bff2..cd98aaf8d75 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" +- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 7c3ac6652ee..03159f123f3 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" +- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" %h3.page-title Edit Merge Request #{@merge_request.to_reference} -- GitLab From 88adf6c585c0c0ad14def38ba2f37abdcc7e1f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 19 Oct 2016 16:55:28 +0000 Subject: [PATCH 032/109] Merge branch 'dont-touch-fs-on-pipeline-save' into 'master' Keep around commits only on pipeline create ## What does this MR do? Since the pipeline SHA doesn't change, we don't need to update keep arounds every save. ## Why was this MR needed? This is minimal change to fix this: https://gitlab.com/gitlab-org/gitlab-ce/issues/23503 See merge request !6986 --- CHANGELOG.md | 1 + app/models/ci/pipeline.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b680499bae..74f3395e516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Ability to resolve merge request conflicts with editor !6374 - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) + - Keep around commits only pipeline creation as pipeline data doesn't change over time - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - Add group level labels. (!6425) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e75fe6c222b..e84c91b417d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -19,7 +19,7 @@ module Ci validates_presence_of :status, unless: :importing? validate :valid_commit_sha, unless: :importing? - after_save :keep_around_commits, unless: :importing? + after_create :keep_around_commits, unless: :importing? delegate :stages, to: :statuses -- GitLab From eb9d321e40a7599b92a0b5b5d5e1af7a3cf1f615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 20 Oct 2016 14:53:08 +0000 Subject: [PATCH 033/109] Merge branch 'update-duration-at-the-end-of-pipeline' into 'master' Update duration at the end of pipeline ## What does this MR do? Moves duration calculation to be done only once at the end of pipeline processing. Currently this is done every one build. ## Why was this MR needed? This is the simplest thing that we can do before properly implementing duration calculation: https://gitlab.com/gitlab-org/gitlab-ce/issues/23523#note_17145614 This is ~Performance improvement that significantly affects: http://performance.gitlab.net/dashboard/db/sidekiq-workers?var-worker=PipelineUpdateWorker%23perform&var-database=Production&from=now-1h&to=now See merge request !6987 --- CHANGELOG.md | 1 + app/models/ci/pipeline.rb | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f3395e516..2db42320278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - Keep around commits only pipeline creation as pipeline data doesn't change over time + - Update duration at the end of pipeline - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - Add group level labels. (!6425) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e84c91b417d..d5c1e03b461 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -59,9 +59,6 @@ module Ci before_transition any => [:success, :failed, :canceled] do |pipeline| pipeline.finished_at = Time.now - end - - before_transition do |pipeline| pipeline.update_duration end -- GitLab From ce8ee22e7590937e3c74b0d14c2d7d6fbe3015da Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 20 Oct 2016 14:03:10 +0000 Subject: [PATCH 034/109] Merge branch '23341-fix-viewing-mr-from-deleted-project' into 'master' Fix a 500 error viewing an MR with a deleted source project ## What does this MR do? Allows merged MRs to be shown without any 500 errors if the source project is removed ## Are there points in the code the reviewer needs to double check? https://gitlab.com/gitlab-org/gitlab-ce/commit/31c37c6c38258684fc92e0d91119c33872e39034 fixed this for closed MRs only. I had trouble understanding the introduced helper and logic, so reverted it and keyed everything on the existence of the source project or branch directly. commits.json returns a 500 error for a closed or merged MR; the approach taken in the above MR was to hide the commits... tab, so I've run with that. For merged MRs, the commits (but not the pipeline data) are in the target project, so we *could* do better, but it's a fairly nasty intervention to make it happen. ## Why was this MR needed? Viewing merged MRs should work even if the fork they came from has been deleted or unlinked. ## Screenshots (if relevant) ![Screen_Shot_2016-10-19_at_17.56.37](/uploads/1aeadd5147b9a4ad29b946b1c7ea52cb/Screen_Shot_2016-10-19_at_17.56.37.png) ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added - [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [x] API support added - Tests - [x] Added for this feature/bug - [ ] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #23341 See merge request !6991 --- CHANGELOG.md | 1 + .../projects/merge_requests_controller.rb | 4 +- app/helpers/merge_requests_helper.rb | 10 +++-- app/models/merge_request.rb | 25 ++++-------- .../projects/merge_requests/_show.html.haml | 24 +++++------ .../merge_requests/created_from_fork_spec.rb | 14 +++++++ spec/models/merge_request_spec.rb | 40 ++----------------- 7 files changed, 48 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db42320278..6f6378dbb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Fix Error 500 when viewing old merge requests with bad diff data - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) + - Fix viewing merged MRs when the source project has been removed !6991 - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0f593d1a936..2ee53f7ceda 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -398,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController status ||= "preparing" else - ci_service = @merge_request.source_project.ci_service + ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service if ci_service.respond_to?(:commit_coverage) @@ -554,7 +554,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - if @pipelines.any? + if @pipelines.present? @pipeline = @pipelines.first @statuses = @pipeline.statuses.relevant end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 249cb44e9d5..a6659ea2fd1 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -86,11 +86,15 @@ module MergeRequestsHelper end def source_branch_with_namespace(merge_request) - branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + namespace = merge_request.source_project_namespace + branch = merge_request.source_branch + + if merge_request.source_branch_exists? + namespace = link_to(namespace, project_path(merge_request.source_project)) + branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + end if merge_request.for_fork? - namespace = link_to(merge_request.source_project_namespace, - project_path(merge_request.source_project)) namespace + ":" + branch else branch diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0cc0b3c2a0e..c476a3bb14e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -326,21 +326,17 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project return true if target_project == source_project - return true unless forked_source_project_missing? + return true unless source_project_missing? errors.add :validate_fork, 'Source project is not a fork of the target project' end def closed_without_fork? - closed? && forked_source_project_missing? + closed? && source_project_missing? end - def closed_without_source_project? - closed? && !source_project - end - - def forked_source_project_missing? + def source_project_missing? return false unless for_fork? return true unless source_project @@ -348,9 +344,7 @@ class MergeRequest < ActiveRecord::Base end def reopenable? - return false if closed_without_fork? || closed_without_source_project? || merged? - - closed? + closed? && !source_project_missing? && source_branch_exists? end def ensure_merge_request_diff @@ -662,7 +656,7 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - source_project.ci_service && commits.any? + source_project.try(:ci_service) && commits.any? end def branch_missing? @@ -694,12 +688,9 @@ class MergeRequest < ActiveRecord::Base @environments ||= begin - environments = source_project.environments_for( - source_branch, diff_head_commit) - environments += target_project.environments_for( - target_branch, diff_head_commit, with_tags: true) - - environments.uniq + envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true) + envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project + envs.uniq end end diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index cd98aaf8d75..0e19d224fcd 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,19 +26,19 @@ %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - - unless @merge_request.closed_without_fork? - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - - if @merge_request.open? && @merge_request.diverged_from_target_branch? - %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - - unless @merge_request.closed_without_source_project? + - if @merge_request.source_branch_exists? = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + + = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default.append-bottom-default @@ -52,7 +52,7 @@ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count - - unless @merge_request.closed_without_source_project? + - if @merge_request.source_project %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do Commits diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index a506624b30d..cfc1244429f 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -25,6 +25,20 @@ feature 'Merge request created from fork' do expect(page).to have_content 'Test merge request' end + context 'source project is deleted' do + background do + MergeRequests::MergeService.new(project, user).execute(merge_request) + fork_project.destroy! + end + + scenario 'user can access merge request' do + visit_merge_request(merge_request) + + expect(page).to have_content 'Test merge request' + expect(page).to have_content "(removed):#{merge_request.source_branch}" + end + end + context 'pipeline present in source project' do include WaitForAjax diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6db5e7f7d80..6e5137602aa 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1198,7 +1198,7 @@ describe MergeRequest, models: true do end end - describe "#forked_source_project_missing?" do + describe "#source_project_missing?" do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:user) { create(:user) } @@ -1211,13 +1211,13 @@ describe MergeRequest, models: true do target_project: project) end - it { expect(merge_request.forked_source_project_missing?).to be_falsey } + it { expect(merge_request.source_project_missing?).to be_falsey } end context "when the source project is the same as the target project" do let(:merge_request) { create(:merge_request, source_project: project) } - it { expect(merge_request.forked_source_project_missing?).to be_falsey } + it { expect(merge_request.source_project_missing?).to be_falsey } end context "when the fork does not exist" do @@ -1231,7 +1231,7 @@ describe MergeRequest, models: true do unlink_project.execute merge_request.reload - expect(merge_request.forked_source_project_missing?).to be_truthy + expect(merge_request.source_project_missing?).to be_truthy end end end @@ -1274,38 +1274,6 @@ describe MergeRequest, models: true do end end - describe '#closed_without_source_project?' do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } - let(:destroy_service) { Projects::DestroyService.new(fork_project, user) } - - context 'when the merge request is closed' do - let(:closed_merge_request) do - create(:closed_merge_request, - source_project: fork_project, - target_project: project) - end - - it 'returns false if the source project exists' do - expect(closed_merge_request.closed_without_source_project?).to be_falsey - end - - it 'returns true if the source project does not exist' do - destroy_service.execute - closed_merge_request.reload - - expect(closed_merge_request.closed_without_source_project?).to be_truthy - end - end - - context 'when the merge request is open' do - it 'returns false' do - expect(subject.closed_without_source_project?).to be_falsey - end - end - end - describe '#reopenable?' do context 'when the merge request is closed' do it 'returns true' do -- GitLab From 3219515f70d7bec6a04f0bf480bbec42cba2f0bb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 17:56:48 +0000 Subject: [PATCH 035/109] Merge branch 'update-omniauth-saml' into 'master' Bump `omniauth-saml` to 1.7.0 Bump `omniauth-saml` to 1.7.0 to include security fixes and metadata support for IdP auto-configuration. cc @dblessing @dewetblomerus See merge request !6997 --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 05166b6a828..46245ab62d1 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos -gem 'omniauth-saml', '~> 1.6.0' +gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index a9892d1c130..442184b9228 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -473,9 +473,9 @@ GEM omniauth-oauth2 (1.3.1) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-saml (1.6.0) + omniauth-saml (1.7.0) omniauth (~> 1.3) - ruby-saml (~> 1.3) + ruby-saml (~> 1.4) omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) omniauth-twitter (1.2.1) @@ -635,7 +635,7 @@ GEM crack (~> 0.4) ruby-prof (0.16.2) ruby-progressbar (1.8.1) - ruby-saml (1.3.0) + ruby-saml (1.4.1) nokogiri (>= 1.5.10) ruby_parser (3.8.2) sexp_processor (~> 4.1) @@ -915,7 +915,7 @@ DEPENDENCIES omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.4.1) omniauth-kerberos (~> 0.3.0) - omniauth-saml (~> 1.6.0) + omniauth-saml (~> 1.7.0) omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) -- GitLab From 8026e0163c82ac919d2f962088252ac628a40608 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 19 Oct 2016 20:36:09 +0000 Subject: [PATCH 036/109] Merge branch 'dropdowns-should-drop-down' into 'master' Remove show_menu_above attribute from issuable dropdowns. Removes `show_menu_above` attribute form issueable dropdowns so they will drop down. Fixes #23425 and #23317 See merge request !7001 --- app/views/shared/issuable/_form.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 34c66a17303..d410755cad1 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -88,19 +88,19 @@ - if issuable.assignee_id = f.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } }) + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - if has_due_date .col-lg-6 .form-group -- GitLab From 0e1a948223f60776d796a65e3a59c98f08d6da7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 20 Oct 2016 10:10:00 +0000 Subject: [PATCH 037/109] Merge branch '23555-project-api-doc' into 'master' Resolve "Breaking a parameter table in Projects API doc" ## What does this MR do? - A broken markdown in `Search for projects by name` is fixed. - cf. !6681 - Pagination is removed from `Search for projects by name` like other docs as it is shown in [/api/README.html](https://docs.gitlab.com/ce/api/README.html) ## Moving docs to a new location? No. ## What are the relevant issue numbers? Closes #23555 See merge request !7007 --- CHANGELOG.md | 1 + doc/api/projects.md | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f6378dbb41..552f41272d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Delete dynamic environments - Fix buggy iOS tooltip layering behavior. - Make guests unable to view MRs on private projects + - Fix broken Project API docs (Takuya Noguchi) ## 8.12.7 diff --git a/doc/api/projects.md b/doc/api/projects.md index b7791b4748a..b69db90e70d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1333,8 +1333,6 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `query` (required) - A string contained in the project name -| `per_page` (optional) - number of projects to return per page -| `page` (optional) - the page to retrieve -| `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields +| `query` | string | yes | A string contained in the project name | +| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | | `sort` | string | no | Return requests sorted in `asc` or `desc` order | -- GitLab From 4303ed620e3fdc64505123e1af1e7bd109d99b9c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 20 Oct 2016 11:58:00 +0000 Subject: [PATCH 038/109] Merge branch 'fix-label-api-spec' into 'master' Make label API spec independent of order See merge request !7013 --- spec/requests/api/labels_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 1da9988978b..867bc615b97 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -22,8 +22,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq(2) - expect(json_response.first['name']).to eq(group_label.name) - expect(json_response.second['name']).to eq(label1.name) + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name]) end end -- GitLab From 734f58a78a5ffe72bf651451e777fccace985f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 20 Oct 2016 12:30:47 +0000 Subject: [PATCH 039/109] Merge branch 'project-cache-worker-lease' into 'master' Restrict ProjectCacheWorker jobs to one per 15 min ## What does this MR do? This restricts performing ProjectCacheWorker jobs to once every 15 minutes per project. The goal of this is to reduce disk load in the event of a lot of pushes happening in a short period of time. The impact is that cached data (e.g. commit count) may be updated less frequently. Furthermore it could mean that separate push payloads won't lead to e.g. READMEs being updated. Most of the cached data isn't updated very frequently, so this trade off should be worth it considering the alternative is a burning storage layer. In the future we'll have to flush caches in a smarter way. For example, refreshing the README cache could probably be done separately from the other caches with its own lease and such. See https://gitlab.com/gitlab-org/gitlab-ce/issues/23550 for more information. See merge request !7017 --- CHANGELOG.md | 5 +++ app/workers/project_cache_worker.rb | 27 ++++++++++++++++ spec/workers/project_cache_worker_spec.rb | 38 +++++++++++++++++------ 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 552f41272d6..4f420cf5d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. +## 8.14.0 (2016-11-22) + - Adds user project membership expired event to clarify why user was removed (Callum Dryden) + - Simpler arguments passed to named_route on toggle_award_url helper method + ## 8.13.0 (2016-10-22) - Fix save button on project pipeline settings page. (!6955) @@ -33,6 +37,7 @@ Please view this file on the master branch, on stable branches it's out of date. - AbstractReferenceFilter caches project_refs on RequestStore when active - Replaced the check sign to arrow in the show build view. !6501 - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) + - ProjectCacheWorker updates caches at most once per 15 minutes per project - Fix Error 500 when viewing old merge requests with bad diff data - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - Fix viewing merged MRs when the source project has been removed !6991 diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index ccefd0f71a0..0d524e88dc3 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -1,9 +1,30 @@ +# Worker for updating any project specific caches. +# +# This worker runs at most once every 15 minutes per project. This is to ensure +# that multiple instances of jobs for this worker don't hammer the underlying +# storage engine as much. class ProjectCacheWorker include Sidekiq::Worker sidekiq_options queue: :default + LEASE_TIMEOUT = 15.minutes.to_i + def perform(project_id) + if try_obtain_lease_for(project_id) + Rails.logger. + info("Obtained ProjectCacheWorker lease for project #{project_id}") + else + Rails.logger. + info("Could not obtain ProjectCacheWorker lease for project #{project_id}") + + return + end + + update_caches(project_id) + end + + def update_caches(project_id) project = Project.find(project_id) return unless project.repository.exists? @@ -15,4 +36,10 @@ class ProjectCacheWorker project.repository.build_cache end end + + def try_obtain_lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT). + try_obtain + end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 5785a6a06ff..f5b60b90d11 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -6,21 +6,39 @@ describe ProjectCacheWorker do subject { described_class.new } describe '#perform' do - it 'updates project cache data' do - expect_any_instance_of(Repository).to receive(:size) - expect_any_instance_of(Repository).to receive(:commit_count) + context 'when an exclusive lease can be obtained' do + before do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(true) + end - expect_any_instance_of(Project).to receive(:update_repository_size) - expect_any_instance_of(Project).to receive(:update_commit_count) + it 'updates project cache data' do + expect_any_instance_of(Repository).to receive(:size) + expect_any_instance_of(Repository).to receive(:commit_count) - subject.perform(project.id) + expect_any_instance_of(Project).to receive(:update_repository_size) + expect_any_instance_of(Project).to receive(:update_commit_count) + + subject.perform(project.id) + end + + it 'handles missing repository data' do + expect_any_instance_of(Repository).to receive(:exists?).and_return(false) + expect_any_instance_of(Repository).not_to receive(:size) + + subject.perform(project.id) + end end - it 'handles missing repository data' do - expect_any_instance_of(Repository).to receive(:exists?).and_return(false) - expect_any_instance_of(Repository).not_to receive(:size) + context 'when an exclusive lease can not be obtained' do + it 'does nothing' do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(false) + + expect(subject).not_to receive(:update_caches) - subject.perform(project.id) + subject.perform(project.id) + end end end end -- GitLab From c1ba29981e9dfe2306358db89aa0c21008f5e59e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 20 Oct 2016 14:54:55 +0000 Subject: [PATCH 040/109] Merge branch 'security-fix-leaking-namespace-name' into 'security' Check that user has access to a given namespace to prevent leaking namespace names. See merge request !2009 --- app/controllers/import/gitlab_projects_controller.rb | 4 ++-- app/views/import/gitlab_projects/new.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 3ec173abcdb..36d246d185b 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -2,8 +2,8 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled def new - @namespace_id = project_params[:namespace_id] - @namespace_name = Namespace.find(project_params[:namespace_id]).name + @namespace = Namespace.find(project_params[:namespace_id]) + return render_404 unless current_user.can?(:create_projects, @namespace) @path = project_params[:path] end diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 44e2653ca4a..767dffb5589 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -9,12 +9,12 @@ %p Project will be imported as %strong - #{@namespace_name}/#{@path} + #{@namespace.name}/#{@path} %p To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. .form-group - = hidden_field_tag :namespace_id, @namespace_id + = hidden_field_tag :namespace_id, @namespace.id = hidden_field_tag :path, @path = label_tag :file, class: 'control-label' do %span GitLab project export -- GitLab From 49643da56e755ec06d2ee4cb642f97a5f2548ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 20 Oct 2016 20:20:33 -0300 Subject: [PATCH 041/109] Update VERSION to 8.13.0-rc4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 4854ca0b005..a6f334050b2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc3 +8.13.0-rc4 -- GitLab From fdd05028ba2769e02b8738a3f303996b1dfaffd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 21 Oct 2016 11:12:33 +0000 Subject: [PATCH 042/109] Merge branch 'patch-1' into 'master' [Doc] Fix `ref` parameter name for `commits/statuses` The attribute to filter by branch or tag needs to be named `ref`, not `ref_name`. And indeed the attribute in the JSON response is `ref` (and not `ref_name`). Tested on Gitlab CE 8.9. See merge request !4876 --- doc/api/commits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/commits.md b/doc/api/commits.md index 6e0882a94de..e1ed99d98d3 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -319,7 +319,7 @@ GET /projects/:id/repository/commits/:sha/statuses | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA -| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch +| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch | `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` | `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit` | `all` | boolean | no | Return all statuses, not only the latest ones -- GitLab From 4265ba8441b364a78f3362f2929b35ea94a8642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 21 Oct 2016 14:58:26 +0000 Subject: [PATCH 043/109] Merge branch 'runners-paginate' into 'master' Fixes error 500 on Runners page (`ActionView::Template::Error (Missing partial kaminari/_paginator`) Originally reported downstream: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=819903 See merge request !5701 --- app/views/admin/runners/index.html.haml | 2 +- app/views/admin/runners/show.html.haml | 2 +- app/views/projects/runners/_specific_runners.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index b760b42fde0..37bb6a3b0e0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -75,4 +75,4 @@ - @runners.each do |runner| = render "admin/runners/runner", runner: runner - = paginate @runners + = paginate @runners, theme: "gitlab" diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 10fea1996aa..73038164056 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -67,7 +67,7 @@ = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' - = paginate @projects + = paginate @projects, theme: "gitlab" .col-md-6 %h4 Recent builds served by this Runner diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 858af78f7bf..51b0939564e 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -26,4 +26,4 @@ %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners = render partial: 'runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners + = paginate @assignable_runners, theme: "gitlab" -- GitLab From 0831657efa2f843cc14c38f8402d4dee1dc8a2a4 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 19 Oct 2016 17:39:31 +0000 Subject: [PATCH 044/109] Merge branch '22457-reset-filters-button-should-be-invisible-when-no-filters-are-active' into 'master' `Reset filters` link should only be visible when filters are active ## Why was this MR needed? `Reset filters` link is always visible. Closes #22457 See merge request !6497 --- app/helpers/issuables_helper.rb | 4 ++++ app/views/shared/issuable/_filter.html.haml | 5 +++-- spec/features/issues/reset_filters_spec.rb | 8 ++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 692fadd505f..03b2db1bc91 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -124,6 +124,10 @@ module IssuablesHelper end end + def issuable_filters_present + params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name] + end + def issuables_count_for_state(issuable_type, state) issuables_finder = public_send("#{issuable_type}_finder") issuables_finder.params[:state] = state diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 8c2036a1cde..ed93857e6d4 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -29,8 +29,9 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - .filter-item.inline.reset-filters - %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters + - if issuable_filters_present + .filter-item.inline.reset-filters + %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters .pull-right - if boards_page diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb index f4d0f13c3d5..c9a3ecf16ea 100644 --- a/spec/features/issues/reset_filters_spec.rb +++ b/spec/features/issues/reset_filters_spec.rb @@ -75,6 +75,14 @@ feature 'Issues filter reset button', feature: true, js: true do end end + context 'when no filters have been applied' do + it 'the reset link should not be visible' do + visit_issues(project) + expect(page).to have_css('.issue', count: 2) + expect(page).not_to have_css '.reset_filters' + end + end + def reset_filters find('.reset-filters').click end -- GitLab From c8859c6839ccab8e726b1d0f5f2b772058e94af5 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 20 Oct 2016 17:55:54 +0000 Subject: [PATCH 045/109] Merge branch 'protected-branches-bundle' into 'master' Create protected branches bundle Backport changes from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/645 See merge request !6909 --- .../protected_branch_access_dropdown.js.es6 | 0 .../{ => protected_branches}/protected_branch_create.js.es6 | 0 .../{ => protected_branches}/protected_branch_dropdown.js.es6 | 0 .../{ => protected_branches}/protected_branch_edit.js.es6 | 0 .../{ => protected_branches}/protected_branch_edit_list.js.es6 | 0 .../javascripts/protected_branches/protected_branches_bundle.js | 1 + app/views/projects/protected_branches/index.html.haml | 2 ++ config/application.rb | 1 + 8 files changed, 4 insertions(+) rename app/assets/javascripts/{ => protected_branches}/protected_branch_access_dropdown.js.es6 (100%) rename app/assets/javascripts/{ => protected_branches}/protected_branch_create.js.es6 (100%) rename app/assets/javascripts/{ => protected_branches}/protected_branch_dropdown.js.es6 (100%) rename app/assets/javascripts/{ => protected_branches}/protected_branch_edit.js.es6 (100%) rename app/assets/javascripts/{ => protected_branches}/protected_branch_edit_list.js.es6 (100%) create mode 100644 app/assets/javascripts/protected_branches/protected_branches_bundle.js diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 similarity index 100% rename from app/assets/javascripts/protected_branch_access_dropdown.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 similarity index 100% rename from app/assets/javascripts/protected_branch_create.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_create.js.es6 diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 similarity index 100% rename from app/assets/javascripts/protected_branch_dropdown.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 similarity index 100% rename from app/assets/javascripts/protected_branch_edit.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 similarity index 100% rename from app/assets/javascripts/protected_branch_edit_list.js.es6 rename to app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js new file mode 100644 index 00000000000..15b3affd469 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -0,0 +1 @@ +/*= require_tree . */ diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 49dcc9a6ba4..42e9bdbd30e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,4 +1,6 @@ - page_title "Protected branches" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/config/application.rb b/config/application.rb index 8a9c539cb43..f3337b00dc6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -87,6 +87,7 @@ module Gitlab config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "profile/profile_bundle.js" + config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" -- GitLab From 59f8e596ddb380421eeca4092bc4c03e4c35bbf6 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 21 Oct 2016 12:51:44 +0000 Subject: [PATCH 046/109] Merge branch 'fix_project_member_access_levels' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix project member access levels Migrate invalid project members (owner -> master) Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/18616 See merge request !6957 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + ...61018124658_make_project_owners_masters.rb | 15 ++++++++ db/schema.rb | 2 +- .../project_members_controller_spec.rb | 36 +++++++++++++++++++ spec/requests/api/members_spec.rb | 11 ++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20161018124658_make_project_owners_masters.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f420cf5d8d..b9b7db57190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix buggy iOS tooltip layering behavior. - Make guests unable to view MRs on private projects - Fix broken Project API docs (Takuya Noguchi) + - Migrate invalid project members (owner -> master) ## 8.12.7 diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb new file mode 100644 index 00000000000..a576bb7b622 --- /dev/null +++ b/db/migrate/20161018124658_make_project_owners_masters.rb @@ -0,0 +1,15 @@ +class MakeProjectOwnersMasters < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + update_column_in_batches(:members, :access_level, 40) do |table, query| + query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project'))) + end + end + + def down + # do nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index a3c7fc2fd57..f5c01511195 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -843,7 +843,7 @@ ActiveRecord::Schema.define(version: 20161019213545) do t.integer "builds_access_level" t.datetime "created_at" t.datetime "updated_at" - t.integer "repository_access_level", default: 20, null: false + t.integer "repository_access_level", default: 20, null: false end add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 074f85157de..9128224c7c5 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -271,4 +271,40 @@ describe Projects::ProjectMembersController do end end end + + describe 'POST create' do + let(:stranger) { create(:user) } + + context 'when creating owner' do + before do + project.team << [user, :master] + sign_in(user) + end + + it 'does not create a member' do + expect do + post :create, user_ids: stranger.id, + namespace_id: project.namespace, + access_level: Member::OWNER, + project_id: project + end.to change { project.members.count }.by(0) + end + end + + context 'when create master' do + before do + project.team << [user, :master] + sign_in(user) + end + + it 'creates a member' do + expect do + post :create, user_ids: stranger.id, + namespace_id: project.namespace, + access_level: Member::MASTER, + project_id: project + end.to change { project.members.count }.by(1) + end + end + end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index d22e0595788..493c0a893d1 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -328,4 +328,15 @@ describe API::Members, api: true do it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do let(:source) { group } end + + context 'Adding owner to project' do + it 'returns 403' do + expect do + post api("/projects/#{project.id}/members", master), + user_id: stranger.id, access_level: Member::OWNER + + expect(response).to have_http_status(422) + end.to change { project.members.count }.by(0) + end + end end -- GitLab From b523b6aed34e187ff677d5347e8223685c742414 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 21 Oct 2016 14:54:30 +0000 Subject: [PATCH 047/109] Merge branch 'fixed-mr-tabs-fixes' into 'master' Fixed issues with sticky mr tabs & sidebar ## What does this MR do? - Fixes an issue where opening the sidebar wouldn't update the merge request tabs width & positioning - Fixes issues when resizing the browser Rather than updating the JS to react to different methods, this way allows the CSS to keep control of the positioning & sizes. ## What are the relevant issue numbers? Closes #23504 See merge request !6990 --- app/assets/javascripts/merge_request_tabs.js | 21 +++--- app/assets/stylesheets/framework/sidebar.scss | 8 +++ .../stylesheets/pages/merge_requests.scss | 13 +++- .../projects/merge_requests/_show.html.haml | 68 ++++++++++--------- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index fd21aa1fefa..9f28738e06b 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -388,28 +388,25 @@ // So we dont affix the tabs on these if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - var tabsWidth = $tabs.outerWidth(), - $diffTabs = $('#diff-notes-app'), - offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height()); + var $diffTabs = $('#diff-notes-app'), + $fixedNav = $('.navbar-fixed-top'), + $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { - top: offsetTop + top: function () { + var tabsTop = $diffTabs.offset().top - $tabs.height(); + tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); + + return tabsTop; + } } }).on('affix.bs.affix', function () { - $tabs.css({ - left: $tabs.offset().left, - width: tabsWidth - }); $diffTabs.css({ marginTop: $tabs.height() }); }).on('affix-top.bs.affix', function () { - $tabs.css({ - left: '', - width: '' - }); $diffTabs.css({ marginTop: '' }); diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ec52f326eb9..1d8e64a0e4b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -185,6 +185,10 @@ header.header-sidebar-pinned { @media (min-width: $screen-sm-min) { padding-right: $sidebar_collapsed_width; + + .merge-request-tabs-holder.affix { + right: $sidebar_collapsed_width; + } } .sidebar-collapsed-icon { @@ -207,6 +211,10 @@ header.header-sidebar-pinned { @media (min-width: $screen-md-min) { padding-right: $gutter_width; + + .merge-request-tabs-holder.affix { + right: $gutter_width; + } } &.with-overlay { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 35a1877df95..70afa568554 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -183,11 +183,11 @@ .ci-coverage { float: right; } - + .stop-env-container { color: $gl-text-color; float: right; - + a { color: $gl-text-color; } @@ -438,11 +438,18 @@ } } -.merge-request-tabs { +.merge-request-tabs-holder { background-color: #fff; &.affix { top: 100px; + left: 0; z-index: 9; + transition: right .15s; + } + + &:not(.affix) .container-fluid { + padding-left: 0; + padding-right: 0; } } diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0e19d224fcd..f57abe73977 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -47,39 +47,41 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits_count.nonzero? - %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipeline - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved - = render "discussions/jump_to_next" + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + %div{ class: container_class } + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.mr_and_commit_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipeline + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved + = render "discussions/jump_to_next" .tab-content#diff-notes-app #notes.notes.tab-pane.voting_notes -- GitLab From 1eeaba10e0684ebc145f618c56b2e9c27d335b54 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 20 Oct 2016 18:00:19 +0000 Subject: [PATCH 048/109] Merge branch 'pass-namespace-gitlab-project-import' into 'master' Fix GitLab project import when a user has access only to their default namespace ## What does this MR do? It fixes a bug when a namespace ID was not passed to `/import/gitlab_project/new` page. It occurred when a user have no choice of the namespace, so we did not render the input for namespace ID. This MR introduces a hidden input for the described case. ## Does this MR meet the acceptance criteria? - Tests - [x] Added for this feature/bug - [x] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Fixes #23507 See merge request !6995 --- app/views/projects/new.html.haml | 1 + .../import_export/import_file_spec.rb | 53 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc8cb134fb8..399ccf15b7f 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -27,6 +27,7 @@ - else .input-group-addon.static-namespace #{root_url}#{current_user.username}/ + = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.col-xs-12.col-sm-6.project-path = f.label :namespace_id, class: 'label-light' do %span diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index f32834801a0..3015576f6f8 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -3,13 +3,8 @@ require 'spec_helper' feature 'Import/Export - project import integration test', feature: true, js: true do include Select2Helper - let(:admin) { create(:admin) } - let(:normal_user) { create(:user) } - let!(:namespace) { create(:namespace, name: "asd", owner: admin) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } - let(:project) { Project.last } - let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -19,41 +14,43 @@ feature 'Import/Export - project import integration test', feature: true, js: tr FileUtils.rm_rf(export_path, secure: true) end - context 'admin user' do + context 'when selecting the namespace' do + let(:user) { create(:admin) } + let!(:namespace) { create(:namespace, name: "asd", owner: user) } + before do - login_as(admin) + login_as(user) end scenario 'user imports an exported project successfully' do - expect(Project.all.count).to be_zero - visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') fill_in :project_path, with: 'test-project-path', visible: true click_link 'GitLab export' expect(page).to have_content('GitLab project export') - expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path') + expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path") attach_file('file', file) - click_on 'Import project' # import starts + expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1) + project = Project.last expect(project).not_to be_nil expect(project.issues).not_to be_empty expect(project.merge_requests).not_to be_empty - expect(project_hook).to exist - expect(wiki_exists?).to be true + expect(project_hook_exists?(project)).to be true + expect(wiki_exists?(project)).to be true expect(project.import_status).to eq('finished') end scenario 'invalid project' do - project = create(:project, namespace_id: 2) + project = create(:project, namespace: namespace) visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') fill_in :project_path, with: project.name, visible: true click_link 'GitLab export' @@ -66,11 +63,11 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end scenario 'project with no name' do - create(:project, namespace_id: 2) + create(:project, namespace: namespace) visit new_project_path - select2('2', from: '#project_namespace_id') + select2(namespace.id, from: '#project_namespace_id') # click on disabled element find(:link, 'GitLab export').trigger('click') @@ -81,24 +78,30 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end end - context 'normal user' do + context 'when limited to the default user namespace' do + let(:user) { create(:user) } before do - login_as(normal_user) + login_as(user) end - scenario 'non-admin user is allowed to import a project' do - expect(Project.all.count).to be_zero - + scenario 'passes correct namespace ID in the URL' do visit new_project_path fill_in :project_path, with: 'test-project-path', visible: true - expect(page).to have_content('GitLab export') + click_link 'GitLab export' + + expect(page).to have_content('GitLab project export') + expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path") end end - def wiki_exists? + def wiki_exists?(project) wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? end + + def project_hook_exists?(project) + Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + end end -- GitLab From 2f0fb20e4ce03e8ab40a1cf1db8e0884c786be9c Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 20 Oct 2016 16:11:25 +0000 Subject: [PATCH 049/109] Merge branch 'preserve-note_type-and-position' into 'master' Preserve note_type and position for notes from emails Closes #23208 See merge request !7010 --- CHANGELOG.md | 1 + .../email/handler/create_note_handler.rb | 4 +++- spec/fixtures/emails/commands_in_reply.eml | 2 -- spec/fixtures/emails/commands_only_reply.eml | 2 -- .../email/handler/create_note_handler_spec.rb | 24 ++++++++++--------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b7db57190..d044aed9976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService + - Fix discussion thread from emails for merge requests. !7010 - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Add tag shortcut from the Commit page. !6543 - Keep refs for each deployment diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb index 06dae31cc27..447c7a6a6b9 100644 --- a/lib/gitlab/email/handler/create_note_handler.rb +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -46,7 +46,9 @@ module Gitlab noteable_type: sent_notification.noteable_type, noteable_id: sent_notification.noteable_id, commit_id: sent_notification.commit_id, - line_code: sent_notification.line_code + line_code: sent_notification.line_code, + position: sent_notification.position, + type: sent_notification.note_type ).execute end end diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml index 06bf60ab734..712f6f797b4 100644 --- a/spec/fixtures/emails/commands_in_reply.eml +++ b/spec/fixtures/emails/commands_in_reply.eml @@ -23,8 +23,6 @@ Cool! /close /todo -/due tomorrow - On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta wrote: diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml index aed64224b06..2d2e2f94290 100644 --- a/spec/fixtures/emails/commands_only_reply.eml +++ b/spec/fixtures/emails/commands_only_reply.eml @@ -21,8 +21,6 @@ X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 /close /todo -/due tomorrow - On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta wrote: diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 4909fed6b77..48660d1dd1b 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -12,10 +12,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do let(:email_raw) { fixture_file('emails/valid_reply.eml') } let(:project) { create(:project, :public) } - let(:noteable) { create(:issue, project: project) } let(:user) { create(:user) } + let(:note) { create(:diff_note_on_merge_request, project: project) } + let(:noteable) { note.noteable } - let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + let!(:sent_notification) do + SentNotification.record_note(note, user.id, mail_key) + end context "when the recipient address doesn't include a mail key" do let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } @@ -82,7 +85,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_closed - expect(noteable.due_date).to eq(Date.tomorrow) expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end @@ -100,7 +102,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_open - expect(noteable.due_date).to be_nil expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy end end @@ -117,7 +118,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do expect { receiver.execute }.to change { noteable.notes.count }.by(2) expect(noteable.reload).to be_closed - expect(noteable.due_date).to eq(Date.tomorrow) expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end @@ -138,10 +138,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do it "creates a comment" do expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last + new_note = noteable.notes.last - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include("I could not disagree more.") + expect(new_note.author).to eq(sent_notification.recipient) + expect(new_note.position).to eq(note.position) + expect(new_note.note).to include("I could not disagree more.") end it "adds all attachments" do @@ -160,10 +161,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do shared_examples 'an email that contains a mail key' do |header| it "fetches the mail key from the #{header} header and creates a comment" do expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last + new_note = noteable.notes.last - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include('I could not disagree more.') + expect(new_note.author).to eq(sent_notification.recipient) + expect(new_note.position).to eq(note.position) + expect(new_note.note).to include('I could not disagree more.') end end -- GitLab From e59383af8baabc9cda35128132027dacf553e62f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 21 Oct 2016 15:15:33 +0000 Subject: [PATCH 050/109] Merge branch 'fix-bulk-assign-issues-for-external-issues' into 'master' Ignore external issues when bulk assigning issues to author of merge request. Fixes #23552 See merge request !7020 --- app/services/merge_requests/assign_issues_service.rb | 2 +- .../merge_requests/assign_issues_service_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb index f636e5fec4f..066efa1acc3 100644 --- a/app/services/merge_requests/assign_issues_service.rb +++ b/app/services/merge_requests/assign_issues_service.rb @@ -4,7 +4,7 @@ module MergeRequests @assignable_issues ||= begin if current_user == merge_request.author closes_issues.select do |issue| - !issue.assignee_id? && can?(current_user, :admin_issue, issue) + !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue) end else [] diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index 7aeb95a15ea..5034b6ef33f 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -46,4 +46,16 @@ describe MergeRequests::AssignIssuesService, services: true do it 'assigns these to the merge request owner' do expect { service.execute }.to change { issue.reload.assignee }.to(user) end + + it 'ignores external issues' do + external_issue = ExternalIssue.new('JIRA-123', project) + service = described_class.new( + project, + user, + merge_request: merge_request, + closes_issues: [external_issue] + ) + + expect(service.assignable_issues.count).to eq 0 + end end -- GitLab From f567f75f23037c74fa55b7f98ff7d66de0ff0535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 20 Oct 2016 17:55:38 +0000 Subject: [PATCH 051/109] Merge branch 'zj-use-iid-deployment-refs' into 'master' Use iid deployment refs This fixes the 404, because `find_by` will return nil instead of throwing an error. See merge request !7021 --- app/models/deployment.rb | 4 ++-- app/models/environment.rb | 4 ++-- spec/controllers/projects/merge_requests_controller_spec.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1f8c5fb3d85..91d85c2279b 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :create_ref + after_create :create_ref def commit project.commit(sha) @@ -102,6 +102,6 @@ class Deployment < ActiveRecord::Base private def ref_path - File.join(environment.ref_path, 'deployments', id.to_s) + File.join(environment.ref_path, 'deployments', iid.to_s) end end diff --git a/app/models/environment.rb b/app/models/environment.rb index d575f1dc73a..73f415c0ef0 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -71,8 +71,8 @@ class Environment < ActiveRecord::Base return nil unless ref - deployment_id = ref.split('/').last - deployments.find(deployment_id) + deployment_iid = ref.split('/').last + deployments.find_by(iid: deployment_iid) end def ref_path diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d6980471ea4..940d54f8686 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -913,7 +913,7 @@ describe Projects::MergeRequestsController do end describe 'GET ci_environments_status' do - context 'when the environment is from a forked project' do + context 'the environment is from a forked project' do let!(:forked) { create(:project) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } -- GitLab From 913af87515236904ba1cec7a02c5c0ccf7925da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 21 Oct 2016 12:39:32 +0000 Subject: [PATCH 052/109] Merge branch 'sh-fix-label-uniquness-migration' into 'master' Fix broken label uniqueness label migration The previous implementation of the migration failed on staging because the migration was attempted to remove labels from projects that did not actually have duplicates. This occurred because the SQL query did not account for the project ID when selecting the labels. To replicate the problem: 1. Disable the uniqueness validation in app/models/label.rb. 2. Create a duplicate label "bug" in project A. 3. Create the same label in project B with label "bug". The migration will attempt to remove the label in B even if there are no duplicates. To fix the issue, include the project ID when selecting the labels. Closes #23609 See merge request !7030 --- db/migrate/20161017125927_add_unique_index_to_labels.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb index 16ae38612de..f2b56ebfb7b 100644 --- a/db/migrate/20161017125927_add_unique_index_to_labels.rb +++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb @@ -7,9 +7,9 @@ class AddUniqueIndexToLabels < ActiveRecord::Migration disable_ddl_transaction! def up - select_all('SELECT title, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label| + select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label| label_title = quote_string(label['title']) - duplicated_ids = select_all("SELECT id FROM labels WHERE title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } + duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } label_id = duplicated_ids.first duplicated_ids.delete(label_id) -- GitLab From 019709f8100a5e4fb7b356fb57b4d9ea29f8c4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 21 Oct 2016 09:41:23 +0000 Subject: [PATCH 053/109] Merge branch 'sh-disable-warm-asset-cache-ci' into 'master' Disable warming of the asset cache in Spinach tests under CI I suspect some combination of Knapsack tests cause no regular Rack tests to be loaded (i.e. all JavaScript tests), which leads to the error: ArgumentError: rack-test requires a rack application, but none was given In CI, we precompile all the assets so there is no need to warm the asset cache in any case. Closes #23613 See merge request !7033 --- features/support/capybara.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index fe9e39cf509..dae0d0f918c 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -20,5 +20,5 @@ unless ENV['CI'] || ENV['CI_SERVER'] end Spinach.hooks.before_run do - TestEnv.warm_asset_cache + TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] end -- GitLab From f5f8a605c8a1508eb22fb67aeb635d140b90ef8f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 21 Oct 2016 15:14:08 +0000 Subject: [PATCH 054/109] Merge branch 'adam-fix-group-web-url' into 'master' Change "Group#web_url" to return "/groups/twitter" rather than "/twitter" Fixes #23527 See merge request !7035 --- app/models/group.rb | 2 +- config/routes/group.rb | 33 ++++++++++++++++++--------------- spec/models/group_spec.rb | 6 ++++++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/models/group.rb b/app/models/group.rb index 00a595d2705..6865e610718 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -68,7 +68,7 @@ class Group < Namespace end def web_url - Gitlab::Routing.url_helpers.group_url(self) + Gitlab::Routing.url_helpers.group_canonical_url(self) end def human_name diff --git a/config/routes/group.rb b/config/routes/group.rb index 4838c9d91c6..826048ba196 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -12,23 +12,26 @@ constraints(GroupUrlConstrainer.new) do end end -resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(? 'groups#show', as: :group_canonical end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ac862055ebc..47f89f744cb 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -265,4 +265,10 @@ describe Group, models: true do members end + + describe '#web_url' do + it 'returns the canonical URL' do + expect(group.web_url).to include("groups/#{group.name}") + end + end end -- GitLab From 17f5c4fce14cbe99226485da20d0a7feb001168c Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 21 Oct 2016 15:03:39 +0000 Subject: [PATCH 055/109] Merge branch 'compare-ellipsis-line' into 'master' Fixed compare ellipsis messing with layout ## What does this MR do? Fixed a bug where the ellipsis would cause the form to mess with the layout. ## Screenshots (if relevant) ![Screen_Shot_2016-10-21_at_13.45.37](/uploads/d1dc18d294dace599b8036541b70ccaf/Screen_Shot_2016-10-21_at_13.45.37.png) See merge request !7042 --- app/views/projects/compare/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 76b68c544aa..7bde20c3286 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -10,7 +10,7 @@ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do .dropdown-toggle-text= params[:from] || 'Select branch/tag' = render "ref_dropdown" - .compare-ellipsis ... + .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to -- GitLab From f75d8a318dd69062b44cd4e556bec45f51a1b588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 21 Oct 2016 18:44:23 +0200 Subject: [PATCH 056/109] Fix RSpec failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/controllers/projects/project_members_controller_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 9128224c7c5..d56de9adbc3 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -273,6 +273,8 @@ describe Projects::ProjectMembersController do end describe 'POST create' do + let(:project) { create(:project) } + let(:user) { create(:user) } let(:stranger) { create(:user) } context 'when creating owner' do -- GitLab From 0aed998d961e24c3a24de8208b1c111315eaa82c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 21 Oct 2016 17:42:50 +0000 Subject: [PATCH 057/109] Merge branch 'separate-sidekiq-queues' into 'master' Use separate queues for all Sidekiq workers ## What does this MR do? This MR updates all workers so that they (mostly) use their own Sidekiq queues. This in turn allows us to monitor queues more accurately and in the future impose queue specific throttles, limits, etc. This is a critical part we need in 8.13, despite it being so close to release day. See https://gitlab.com/gitlab-org/gitlab-ce/issues/23370 for more information. ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added - [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? https://gitlab.com/gitlab-org/gitlab-ce/issues/23370 See merge request !7006 --- CHANGELOG.md | 1 + app/workers/admin_email_worker.rb | 3 +- app/workers/build_coverage_worker.rb | 2 +- app/workers/build_email_worker.rb | 1 + app/workers/build_finished_worker.rb | 1 + app/workers/build_hooks_worker.rb | 2 +- app/workers/build_success_worker.rb | 2 +- app/workers/clear_database_cache_worker.rb | 1 + app/workers/concerns/build_queue.rb | 8 ++ app/workers/concerns/cronjob_queue.rb | 9 ++ .../concerns/dedicated_sidekiq_queue.rb | 9 ++ app/workers/concerns/pipeline_queue.rb | 8 ++ .../concerns/repository_check_queue.rb | 8 ++ app/workers/delete_user_worker.rb | 1 + app/workers/email_receiver_worker.rb | 3 +- app/workers/emails_on_push_worker.rb | 2 +- app/workers/expire_build_artifacts_worker.rb | 1 + .../expire_build_instance_artifacts_worker.rb | 1 + app/workers/git_garbage_collect_worker.rb | 3 +- app/workers/gitlab_shell_worker.rb | 3 +- app/workers/group_destroy_worker.rb | 3 +- .../import_export_project_cleanup_worker.rb | 3 +- app/workers/irker_worker.rb | 1 + app/workers/merge_worker.rb | 3 +- app/workers/new_note_worker.rb | 3 +- app/workers/pipeline_hooks_worker.rb | 2 +- app/workers/pipeline_metrics_worker.rb | 3 +- app/workers/pipeline_process_worker.rb | 3 +- app/workers/pipeline_success_worker.rb | 2 +- app/workers/pipeline_update_worker.rb | 3 +- app/workers/post_receive.rb | 3 +- app/workers/project_cache_worker.rb | 3 +- app/workers/project_destroy_worker.rb | 3 +- app/workers/project_export_worker.rb | 3 +- app/workers/project_service_worker.rb | 3 +- app/workers/project_web_hook_worker.rb | 3 +- app/workers/prune_old_events_worker.rb | 1 + .../remove_expired_group_links_worker.rb | 1 + app/workers/remove_expired_members_worker.rb | 1 + .../repository_archive_cache_worker.rb | 3 +- app/workers/repository_check/batch_worker.rb | 21 ++-- app/workers/repository_check/clear_worker.rb | 3 +- .../single_repository_worker.rb | 3 +- app/workers/repository_fork_worker.rb | 3 +- app/workers/repository_import_worker.rb | 3 +- app/workers/requests_profiles_worker.rb | 3 +- app/workers/stuck_ci_builds_worker.rb | 1 + app/workers/system_hook_worker.rb | 3 +- app/workers/trending_projects_worker.rb | 3 +- app/workers/update_merge_requests_worker.rb | 1 + bin/background_jobs | 3 +- config/application.rb | 3 +- config/sidekiq_queues.yml | 46 ++++++++ ...736_migrate_sidekiq_queues_from_default.rb | 109 ++++++++++++++++++ doc/development/README.md | 3 +- doc/development/sidekiq_style_guide.md | 38 ++++++ spec/workers/concerns/build_queue_spec.rb | 14 +++ spec/workers/concerns/cronjob_queue_spec.rb | 18 +++ .../concerns/dedicated_sidekiq_queue_spec.rb | 20 ++++ spec/workers/concerns/pipeline_queue_spec.rb | 14 +++ .../concerns/repository_check_queue_spec.rb | 18 +++ spec/workers/every_sidekiq_worker_spec.rb | 44 +++++++ 62 files changed, 425 insertions(+), 68 deletions(-) create mode 100644 app/workers/concerns/build_queue.rb create mode 100644 app/workers/concerns/cronjob_queue.rb create mode 100644 app/workers/concerns/dedicated_sidekiq_queue.rb create mode 100644 app/workers/concerns/pipeline_queue.rb create mode 100644 app/workers/concerns/repository_check_queue.rb create mode 100644 config/sidekiq_queues.yml create mode 100644 db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb create mode 100644 doc/development/sidekiq_style_guide.md create mode 100644 spec/workers/concerns/build_queue_spec.rb create mode 100644 spec/workers/concerns/cronjob_queue_spec.rb create mode 100644 spec/workers/concerns/dedicated_sidekiq_queue_spec.rb create mode 100644 spec/workers/concerns/pipeline_queue_spec.rb create mode 100644 spec/workers/concerns/repository_check_queue_spec.rb create mode 100644 spec/workers/every_sidekiq_worker_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d044aed9976..147b9bdd32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.0 (2016-10-22) - Fix save button on project pipeline settings page. (!6955) + - All Sidekiq workers now use their own queue - Avoid race condition when asynchronously removing expired artifacts. (!6881) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Respond with 404 Not Found for non-existent tags (Linus Thiel) diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index 667fff031dd..c2dc955b27c 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -1,7 +1,6 @@ class AdminEmailWorker include Sidekiq::Worker - - sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + include CronjobQueue def perform repository_check_failed_count = Project.where(last_repository_check_failed: true).count diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb index 0680645a8db..def0ab1dde1 100644 --- a/app/workers/build_coverage_worker.rb +++ b/app/workers/build_coverage_worker.rb @@ -1,6 +1,6 @@ class BuildCoverageWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb index 1c7a04a66a8..5fdb1f2baa0 100644 --- a/app/workers/build_email_worker.rb +++ b/app/workers/build_email_worker.rb @@ -1,5 +1,6 @@ class BuildEmailWorker include Sidekiq::Worker + include BuildQueue def perform(build_id, recipients, push_data) recipients.each do |recipient| diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index e7286b77ac5..466410bf08c 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -1,5 +1,6 @@ class BuildFinishedWorker include Sidekiq::Worker + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index e22ececb3fd..9965af935d4 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -1,6 +1,6 @@ class BuildHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index 500d357ce31..e0ad5268664 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -1,6 +1,6 @@ class BuildSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb index c541daba50e..c4cb4733482 100644 --- a/app/workers/clear_database_cache_worker.rb +++ b/app/workers/clear_database_cache_worker.rb @@ -1,6 +1,7 @@ # This worker clears all cache fields in the database, working in batches. class ClearDatabaseCacheWorker include Sidekiq::Worker + include DedicatedSidekiqQueue BATCH_SIZE = 1000 diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb new file mode 100644 index 00000000000..cf0ead40a8b --- /dev/null +++ b/app/workers/concerns/build_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI build workers. +module BuildQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :build + end +end diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb new file mode 100644 index 00000000000..e918bb011e0 --- /dev/null +++ b/app/workers/concerns/cronjob_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets various Sidekiq settings for workers executed using a +# cronjob. +module CronjobQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :cronjob, retry: false + end +end diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb new file mode 100644 index 00000000000..132bae6022b --- /dev/null +++ b/app/workers/concerns/dedicated_sidekiq_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets the queue of a Sidekiq worker based on the worker's class +# name/namespace. +module DedicatedSidekiqQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_') + end +end diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb new file mode 100644 index 00000000000..ca3860e1d38 --- /dev/null +++ b/app/workers/concerns/pipeline_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI pipeline workers. +module PipelineQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :pipeline + end +end diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb new file mode 100644 index 00000000000..a597321ccf4 --- /dev/null +++ b/app/workers/concerns/repository_check_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various repository check workers. +module RepositoryCheckQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :repository_check, retry: false + end +end diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 6ff361e4d80..3194c389b3d 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -1,5 +1,6 @@ class DeleteUserWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(current_user_id, delete_user_id, options = {}) delete_user = User.find(delete_user_id) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 842eebdea9e..d3f7e479a8d 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -1,7 +1,6 @@ class EmailReceiverWorker include Sidekiq::Worker - - sidekiq_options queue: :incoming_email + include DedicatedSidekiqQueue def perform(raw) return unless Gitlab::IncomingEmail.enabled? diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 1dc7e0adef7..b9cd49985dc 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :mailers attr_reader :email, :skip_premailer def perform(project_id, recipients, push_data, options = {}) diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index 174eabff9fd..a27585fd389 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -1,5 +1,6 @@ class ExpireBuildArtifactsWorker include Sidekiq::Worker + include CronjobQueue def perform Rails.logger.info 'Scheduling removal of build artifacts' diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index d9e2cc37bb3..eb403c134d1 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -1,5 +1,6 @@ class ExpireBuildInstanceArtifactsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(build_id) build = Ci::Build diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index a6cefd4d601..65f8093b5b0 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,8 +1,9 @@ class GitGarbageCollectWorker include Sidekiq::Worker include Gitlab::ShellAdapter + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: false + sidekiq_options retry: false def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index cfeda88bbc5..964287a1793 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -1,8 +1,7 @@ class GitlabShellWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(action, *arg) gitlab_shell.send(action, *arg) diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index 5048746f09b..a49a5fd0855 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -1,7 +1,6 @@ class GroupDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(group_id, user_id) begin diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb index 72e3a9ae734..7957ed807ab 100644 --- a/app/workers/import_export_project_cleanup_worker.rb +++ b/app/workers/import_export_project_cleanup_worker.rb @@ -1,7 +1,6 @@ class ImportExportProjectCleanupWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform ImportExportCleanUpService.new.execute diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 19f38358eb5..7e44b241743 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -3,6 +3,7 @@ require 'socket' class IrkerWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, chans, colors, push_data, settings) project = Project.find(project_id) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index c87c0a252b1..79efca4f2f9 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -1,7 +1,6 @@ class MergeWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(merge_request_id, current_user_id, params) params = params.with_indifferent_access diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 1b3232cd365..c3e62bb88c0 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -1,7 +1,6 @@ class NewNoteWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(note_id, note_params) note = Note.find(note_id) diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index ab5e9f6daad..7e36eacebf8 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -1,6 +1,6 @@ class PipelineHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index 7bb92df3bbd..34f6ef161fb 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -1,7 +1,6 @@ class PipelineMetricsWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index f44227d7086..357e4a9a1c3 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,7 +1,6 @@ class PipelineProcessWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 5dd443fea59..2aa6fff24da 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -1,6 +1,6 @@ class PipelineSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index 44a7f24e401..96c4152c674 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -1,7 +1,6 @@ class PipelineUpdateWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index a9a2b716005..eee0ca12af9 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -1,7 +1,6 @@ class PostReceive include Sidekiq::Worker - - sidekiq_options queue: :post_receive + include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) } diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 0d524e88dc3..71b274e0c99 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -5,8 +5,7 @@ # storage engine as much. class ProjectCacheWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue LEASE_TIMEOUT = 15.minutes.to_i diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index 3062301a9b1..b462327490e 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -1,7 +1,6 @@ class ProjectDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(project_id, user_id, params) begin diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index 615311e63f5..6009aa1b191 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -1,7 +1,8 @@ class ProjectExportWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: 3 + sidekiq_options retry: 3 def perform(current_user_id, project_id) current_user = User.find(current_user_id) diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb index 64d39c4d3f7..fdfdeab7b41 100644 --- a/app/workers/project_service_worker.rb +++ b/app/workers/project_service_worker.rb @@ -1,7 +1,6 @@ class ProjectServiceWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data) data = data.with_indifferent_access diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index fb878965288..efb85eafd15 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -1,7 +1,6 @@ class ProjectWebHookWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) data = data.with_indifferent_access diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 5883cafe1d1..392abb9c21b 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -1,5 +1,6 @@ class PruneOldEventsWorker include Sidekiq::Worker + include CronjobQueue def perform # Contribution calendar shows maximum 12 months of events. diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 246c8b6650a..2a619f83410 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredGroupLinksWorker include Sidekiq::Worker + include CronjobQueue def perform ProjectGroupLink.expired.destroy_all diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index cf765af97ce..31f652e5f9b 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredMembersWorker include Sidekiq::Worker + include CronjobQueue def perform Member.expired.find_each do |member| diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb index a2e49c61f59..e47069df189 100644 --- a/app/workers/repository_archive_cache_worker.rb +++ b/app/workers/repository_archive_cache_worker.rb @@ -1,7 +1,6 @@ class RepositoryArchiveCacheWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform RepositoryArchiveCleanUpService.new.execute diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index a3e16fa5212..c3e7491ec4e 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -1,14 +1,13 @@ module RepositoryCheck class BatchWorker include Sidekiq::Worker - + include CronjobQueue + RUN_TIME = 3600 - - sidekiq_options retry: false - + def perform start = Time.now - + # This loop will break after a little more than one hour ('a little # more' because `git fsck` may take a few minutes), or if it runs out of # projects to check. By default sidekiq-cron will start a new @@ -17,15 +16,15 @@ module RepositoryCheck project_ids.each do |project_id| break if Time.now - start >= RUN_TIME break unless current_settings.repository_checks_enabled - + next unless try_obtain_lease(project_id) - + SingleRepositoryWorker.new.perform(project_id) end end - + private - + # Project.find_each does not support WHERE clauses and # Project.find_in_batches does not support ordering. So we just build an # array of ID's. This is OK because we do it only once an hour, because @@ -39,7 +38,7 @@ module RepositoryCheck reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end - + def try_obtain_lease(id) # Use a 24-hour timeout because on servers/projects where 'git fsck' is # super slow we definitely do not want to run it twice in parallel. @@ -48,7 +47,7 @@ module RepositoryCheck timeout: 24.hours ).try_obtain end - + def current_settings # No caching of the settings! If we cache them and an admin disables # this feature, an active RepositoryCheckWorker would keep going for up diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index b7202ddff34..1f1b38540ee 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class ClearWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform # Do small batched updates because these updates will be slow and locking diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index 98ddf5d0688..3d8bfc6fc6c 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class SingleRepositoryWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 61ed1c38ac4..efc99ec962a 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -1,8 +1,7 @@ class RepositoryForkWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(project_id, forked_from_repository_storage_path, source_path, target_path) Gitlab::Metrics.add_event(:fork_repository, diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index d2ca8813ab9..c8a77e21c12 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -1,8 +1,7 @@ class RepositoryImportWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue attr_accessor :project, :current_user diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb index 9dd228a2483..703b025d76e 100644 --- a/app/workers/requests_profiles_worker.rb +++ b/app/workers/requests_profiles_worker.rb @@ -1,7 +1,6 @@ class RequestsProfilesWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform Gitlab::RequestProfiler.remove_all_profiles diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index 6828013b377..b70df5a1afa 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -1,5 +1,6 @@ class StuckCiBuildsWorker include Sidekiq::Worker + include CronjobQueue BUILD_STUCK_TIMEOUT = 1.day diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index a122c274763..baf2f12eeac 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -1,7 +1,6 @@ class SystemHookWorker include Sidekiq::Worker - - sidekiq_options queue: :system_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) SystemHook.find(hook_id).execute(data, hook_name) diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb index df4c4a6628b..0531630d13a 100644 --- a/app/workers/trending_projects_worker.rb +++ b/app/workers/trending_projects_worker.rb @@ -1,7 +1,6 @@ class TrendingProjectsWorker include Sidekiq::Worker - - sidekiq_options queue: :trending_projects + include CronjobQueue def perform Rails.logger.info('Refreshing trending projects') diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 03f0528cdae..acc4d858136 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -1,5 +1,6 @@ class UpdateMergeRequestsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, user_id, oldrev, newrev, ref) project = Project.find_by(id: project_id) diff --git a/bin/background_jobs b/bin/background_jobs index 25a578a1c49..f28e2f722dc 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -4,6 +4,7 @@ cd $(dirname $0)/.. app_root=$(pwd) sidekiq_pidfile="$app_root/tmp/pids/sidekiq.pid" sidekiq_logfile="$app_root/log/sidekiq.log" +sidekiq_config="$app_root/config/sidekiq_queues.yml" gitlab_user=$(ls -l config.ru | awk '{print $3}') warn() @@ -37,7 +38,7 @@ start_no_deamonize() start_sidekiq() { - exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" + exec bundle exec sidekiq -C "${sidekiq_config}" -e $RAILS_ENV -P $sidekiq_pidfile "$@" } load_ok() diff --git a/config/application.rb b/config/application.rb index f3337b00dc6..92c8467e7f4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,7 +24,8 @@ module Gitlab #{config.root}/app/models/ci #{config.root}/app/models/hooks #{config.root}/app/models/members - #{config.root}/app/models/project_services)) + #{config.root}/app/models/project_services + #{config.root}/app/workers/concerns)) config.generators.templates.push("#{config.root}/generator_templates") diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml new file mode 100644 index 00000000000..c2e880e891f --- /dev/null +++ b/config/sidekiq_queues.yml @@ -0,0 +1,46 @@ +# This configuration file should be exclusively used to set queue settings for +# Sidekiq. Any other setting should be specified using the Sidekiq CLI or the +# Sidekiq Ruby API (see config/initializers/sidekiq.rb). +--- +# All the queues to process and their weights. Every queue _must_ have a weight +# defined. +# +# The available weights are as follows +# +# 1: low priority +# 2: medium priority +# 3: high priority +# 5: _super_ high priority, this should only be used for _very_ important queues +# +# As per http://stackoverflow.com/a/21241357/290102 the formula for calculating +# the likelihood of a job being popped off a queue (given all queues have work +# to perform) is: +# +# chance = (queue weight / total weight of all queues) * 100 +:queues: + - [post_receive, 5] + - [merge, 5] + - [update_merge_requests, 3] + - [new_note, 2] + - [build, 2] + - [pipeline, 2] + - [gitlab_shell, 2] + - [email_receiver, 2] + - [emails_on_push, 2] + - [repository_fork, 1] + - [repository_import, 1] + - [project_service, 1] + - [clear_database_cache, 1] + - [delete_user, 1] + - [expire_build_instance_artifacts, 1] + - [group_destroy, 1] + - [irker, 1] + - [project_cache, 1] + - [project_destroy, 1] + - [project_export, 1] + - [project_web_hook, 1] + - [repository_check, 1] + - [system_hook, 1] + - [git_garbage_collect, 1] + - [cronjob, 1] + - [default, 1] diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb new file mode 100644 index 00000000000..e875213ab96 --- /dev/null +++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb @@ -0,0 +1,109 @@ +require 'json' + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + + DOWNTIME_REASON = <<-EOF + Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping + Sidekiq will result in the loss of jobs that are scheduled after this + migration completes. + EOF + + disable_ddl_transaction! + + # Jobs for which the queue names have been changed (e.g. multiple workers + # using the same non-default queue). + # + # The keys are the old queue names, the values the jobs to move and their new + # queue names. + RENAMED_QUEUES = { + gitlab_shell: { + 'GitGarbageCollectorWorker' => :git_garbage_collector, + 'ProjectExportWorker' => :project_export, + 'RepositoryForkWorker' => :repository_fork, + 'RepositoryImportWorker' => :repository_import + }, + project_web_hook: { + 'ProjectServiceWorker' => :project_service + }, + incoming_email: { + 'EmailReceiverWorker' => :email_receiver + }, + mailers: { + 'EmailsOnPushWorker' => :emails_on_push + }, + default: { + 'AdminEmailWorker' => :cronjob, + 'BuildCoverageWorker' => :build, + 'BuildEmailWorker' => :build, + 'BuildFinishedWorker' => :build, + 'BuildHooksWorker' => :build, + 'BuildSuccessWorker' => :build, + 'ClearDatabaseCacheWorker' => :clear_database_cache, + 'DeleteUserWorker' => :delete_user, + 'ExpireBuildArtifactsWorker' => :cronjob, + 'ExpireBuildInstanceArtifactsWorker' => :expire_build_instance_artifacts, + 'GroupDestroyWorker' => :group_destroy, + 'ImportExportProjectCleanupWorker' => :cronjob, + 'IrkerWorker' => :irker, + 'MergeWorker' => :merge, + 'NewNoteWorker' => :new_note, + 'PipelineHooksWorker' => :pipeline, + 'PipelineMetricsWorker' => :pipeline, + 'PipelineProcessWorker' => :pipeline, + 'PipelineSuccessWorker' => :pipeline, + 'PipelineUpdateWorker' => :pipeline, + 'ProjectCacheWorker' => :project_cache, + 'ProjectDestroyWorker' => :project_destroy, + 'PruneOldEventsWorker' => :cronjob, + 'RemoveExpiredGroupLinksWorker' => :cronjob, + 'RemoveExpiredMembersWorker' => :cronjob, + 'RepositoryArchiveCacheWorker' => :cronjob, + 'RepositoryCheck::BatchWorker' => :cronjob, + 'RepositoryCheck::ClearWorker' => :repository_check, + 'RepositoryCheck::SingleRepositoryWorker' => :repository_check, + 'RequestsProfilesWorker' => :cronjob, + 'StuckCiBuildsWorker' => :cronjob, + 'UpdateMergeRequestsWorker' => :update_merge_requests + } + } + + def up + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |queue, jobs| + migrate_from_queue(redis, queue, jobs) + end + end + end + + def down + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |dest_queue, jobs| + jobs.each do |worker, from_queue| + migrate_from_queue(redis, from_queue, worker => dest_queue) + end + end + end + end + + def migrate_from_queue(redis, queue, job_mapping) + while job = redis.lpop("queue:#{queue}") + payload = JSON.load(job) + new_queue = job_mapping[payload['class']] + + # If we have no target queue to migrate to we're probably dealing with + # some ancient job for which the worker no longer exists. In that case + # there's no sane option we can take, other than just dropping the job. + next unless new_queue + + payload['queue'] = new_queue + + redis.lpush("queue:#{new_queue}", JSON.dump(payload)) + end + end +end diff --git a/doc/development/README.md b/doc/development/README.md index 9706cb1de7f..fb6a8a5b095 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -14,7 +14,8 @@ - [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements - [Frontend guidelines](frontend.md) -- [SQL guidelines](sql.md) for SQL guidelines +- [SQL guidelines](sql.md) for working with SQL queries +- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers ## Process diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md new file mode 100644 index 00000000000..e3a20f29a09 --- /dev/null +++ b/doc/development/sidekiq_style_guide.md @@ -0,0 +1,38 @@ +# Sidekiq Style Guide + +This document outlines various guidelines that should be followed when adding or +modifying Sidekiq workers. + +## Default Queue + +Use of the "default" queue is not allowed. Every worker should use a queue that +matches the worker's purpose the closest. For example, workers that are to be +executed periodically should use the "cronjob" queue. + +A list of all available queues can be found in `config/sidekiq_queues.yml`. + +## Dedicated Queues + +Most workers should use their own queue. To ease this process a worker can +include the `DedicatedSidekiqQueue` concern as follows: + +```ruby +class ProcessSomethingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue +end +``` + +This will set the queue name based on the class' name, minus the `Worker` +suffix. In the above example this would lead to the queue being +`process_something`. + +In some cases multiple workers do use the same queue. For example, the various +workers for updating CI pipelines all use the `pipeline` queue. Adding workers +to existing queues should be done with care, as adding more workers can lead to +slow jobs blocking work (even for different jobs) on the shared queue. + +## Tests + +Each Sidekiq worker must be tested using RSpec, just like any other class. These +tests should be placed in `spec/workers`. diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb new file mode 100644 index 00000000000..6bf955e0be2 --- /dev/null +++ b/spec/workers/concerns/build_queue_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe BuildQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include BuildQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('build') + end +end diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb new file mode 100644 index 00000000000..5d1336c21a6 --- /dev/null +++ b/spec/workers/concerns/cronjob_queue_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe CronjobQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include CronjobQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + end + + it 'disables retrying of failed jobs' do + expect(worker.sidekiq_options['retry']).to eq(false) + end +end diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..512baec8b7e --- /dev/null +++ b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DedicatedSidekiqQueue do + let(:worker) do + Class.new do + def self.name + 'Foo::Bar::DummyWorker' + end + + include Sidekiq::Worker + include DedicatedSidekiqQueue + end + end + + describe 'queue names' do + it 'sets the queue name based on the class name' do + expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy') + end + end +end diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb new file mode 100644 index 00000000000..40794d0e42a --- /dev/null +++ b/spec/workers/concerns/pipeline_queue_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe PipelineQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include PipelineQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline') + end +end diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb new file mode 100644 index 00000000000..8868e969829 --- /dev/null +++ b/spec/workers/concerns/repository_check_queue_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe RepositoryCheckQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include RepositoryCheckQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check') + end + + it 'disables retrying of failed jobs' do + expect(worker.sidekiq_options['retry']).to eq(false) + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb new file mode 100644 index 00000000000..fc9adf47c1e --- /dev/null +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Every Sidekiq worker' do + let(:workers) do + root = Rails.root.join('app', 'workers') + concerns = root.join('concerns').to_s + + workers = Dir[root.join('**', '*.rb')]. + reject { |path| path.start_with?(concerns) } + + workers.map do |path| + ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') + + ns.camelize.constantize + end + end + + it 'does not use the default queue' do + workers.each do |worker| + expect(worker.sidekiq_options['queue'].to_s).not_to eq('default') + end + end + + it 'uses the cronjob queue when the worker runs as a cronjob' do + cron_workers = Settings.cron_jobs. + map { |job_name, options| options['job_class'].constantize }. + to_set + + workers.each do |worker| + next unless cron_workers.include?(worker) + + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + end + end + + it 'defines the queue in the Sidekiq configuration file' do + config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s) + queue_names = config[:queues].map { |(queue, _)| queue }.to_set + + workers.each do |worker| + expect(queue_names).to include(worker.sidekiq_options['queue'].to_s) + end + end +end -- GitLab From eb54c711a0c43f768cd46aa2cc1b00f9a9c9a078 Mon Sep 17 00:00:00 2001 From: Alejandro Rodriguez Date: Fri, 21 Oct 2016 22:35:49 +0000 Subject: [PATCH 058/109] Merge branch 'markdown-xss-fix-option-2' into 'security' Don't autolink unsafe protocols Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/23153 See merge request !2013 --- lib/banzai/filter/autolink_filter.rb | 13 +++++++++++ .../lib/banzai/filter/autolink_filter_spec.rb | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 799b83b1069..f076d59d259 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -71,6 +71,11 @@ module Banzai @doc = parse_html(rinku) end + # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme + def contains_unsafe?(scheme) + Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) } + end + # Autolinks any text matching LINK_PATTERN that Rinku didn't already # replace def text_parse @@ -79,6 +84,14 @@ module Banzai next unless content.match(LINK_PATTERN) + begin + uri = Addressable::URI.parse(content) + uri.scheme = uri.scheme.strip.downcase if uri.scheme + next if contains_unsafe?(uri.scheme) + rescue Addressable::URI::InvalidURIError + next + end + html = autolink_filter(content) next if html == content diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index dca7f997570..6d3dd49e780 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do expect(doc.at_css('a')['href']).to eq link end + it 'autolinks rdar' do + link = 'rdar://localhost.com/blah' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not autolink javascript' do + link = 'javascript://alert(document.cookie);' + doc = filter("See #{link}") + + expect(doc.to_s).not_to include('href="javascript://') + end + + it 'does not autolink bad URLs' do + link = 'foo://23423:::asdf' + doc = filter("See #{link}") + + expect(doc.to_s).to eq("See #{link}") + end + it 'does not include trailing punctuation' do doc = filter("See #{link}.") expect(doc.at_css('a').text).to eq link -- GitLab From a30d1c73431872fcda8c4d103040f5e15bb0200c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Fri, 21 Oct 2016 20:58:48 -0300 Subject: [PATCH 059/109] Update VERSION to 8.13.0-rc5 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a6f334050b2..ed165c23e82 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc4 +8.13.0-rc5 -- GitLab From 1a7926cad9e039db496bd2dbfc418ef32c423a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Fri, 21 Oct 2016 22:16:53 -0300 Subject: [PATCH 060/109] Update VERSION to 8.13.0-rc6 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ed165c23e82..7e6a192fc4c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc5 +8.13.0-rc6 -- GitLab From 2289ee1cdde87001d3a458aae2a91679b29a3c63 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 21 Oct 2016 22:18:23 -0700 Subject: [PATCH 061/109] Revert "Merge branch 'markdown-xss-fix-option-2' into 'security' This reverts commit eb54c711a0c43f768cd46aa2cc1b00f9a9c9a078. --- lib/banzai/filter/autolink_filter.rb | 13 ----------- .../lib/banzai/filter/autolink_filter_spec.rb | 22 ------------------- 2 files changed, 35 deletions(-) diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index f076d59d259..799b83b1069 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -71,11 +71,6 @@ module Banzai @doc = parse_html(rinku) end - # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme - def contains_unsafe?(scheme) - Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) } - end - # Autolinks any text matching LINK_PATTERN that Rinku didn't already # replace def text_parse @@ -84,14 +79,6 @@ module Banzai next unless content.match(LINK_PATTERN) - begin - uri = Addressable::URI.parse(content) - uri.scheme = uri.scheme.strip.downcase if uri.scheme - next if contains_unsafe?(uri.scheme) - rescue Addressable::URI::InvalidURIError - next - end - html = autolink_filter(content) next if html == content diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index 6d3dd49e780..dca7f997570 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -99,28 +99,6 @@ describe Banzai::Filter::AutolinkFilter, lib: true do expect(doc.at_css('a')['href']).to eq link end - it 'autolinks rdar' do - link = 'rdar://localhost.com/blah' - doc = filter("See #{link}") - - expect(doc.at_css('a').text).to eq link - expect(doc.at_css('a')['href']).to eq link - end - - it 'does not autolink javascript' do - link = 'javascript://alert(document.cookie);' - doc = filter("See #{link}") - - expect(doc.to_s).not_to include('href="javascript://') - end - - it 'does not autolink bad URLs' do - link = 'foo://23423:::asdf' - doc = filter("See #{link}") - - expect(doc.to_s).to eq("See #{link}") - end - it 'does not include trailing punctuation' do doc = filter("See #{link}.") expect(doc.at_css('a').text).to eq link -- GitLab From 4c0474aa52656df646af2987262d147c520e4204 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 22 Oct 2016 05:51:09 +0000 Subject: [PATCH 062/109] Merge branch 'sh-add-mailers-to-sidekiq-config' into 'master' Fix bug where e-mails were not being sent out via Sidekiq Fix bug where e-mails were not being sent out via Sidekiq By default, ActionMailer uses the "mailers" queue, but this entry was not included in the list of queues for Sidekiq to use. For more details: * https://github.com/plataformatec/devise/wiki/How-To:-Send-devise-emails-in-background-(Resque,-Sidekiq-and-Delayed::Job) * http://guides.rubyonrails.org/active_job_basics.html See merge request !7053 --- config/sidekiq_queues.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index c2e880e891f..f36fe893fd0 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -27,6 +27,7 @@ - [gitlab_shell, 2] - [email_receiver, 2] - [emails_on_push, 2] + - [mailers, 2] - [repository_fork, 1] - [repository_import, 1] - [project_service, 1] -- GitLab From ee9cb0602ac2ed9b6490a49ca84b17fb323ff178 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 21 Oct 2016 22:58:52 -0700 Subject: [PATCH 063/109] Update VERSION to 8.13.0-rc7 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7e6a192fc4c..18d7b9bb2a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc6 +8.13.0-rc7 -- GitLab From 053a0a2ccdc74c2fd2ae400fd73675d0e14b1aba Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 22 Oct 2016 00:18:23 -0700 Subject: [PATCH 064/109] Update VERSION to 8.13.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 18d7b9bb2a5..35de8148559 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0-rc7 +8.13.0 -- GitLab From 577147e0bb4cc72096f41795a3e9a226ae3aaf3c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 24 Oct 2016 19:27:18 +0000 Subject: [PATCH 065/109] Merge branch '23325-pipeline-graph-hidden' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix pipeline graph hidden on commit and mr pages ## What does this MR do? Dynamically invokes `new gl.Pipelines();` when an MR `builds` tab is clicked. Dispatches `new gl.Pipelines();` on a `commit#builds` page. ## Are there points in the code the reviewer needs to double check? ## Why was this MR needed? The pipeline graph was hidden on commit and mr pages ## Screenshots (if relevant) Commit page: ![Screen_Shot_2016-10-14_at_18.16.18](/uploads/ee11dea0825d1489dc167292e16c8f41/Screen_Shot_2016-10-14_at_18.16.18.png) MR: ![Screen_Shot_2016-10-14_at_18.16.39](/uploads/602c2fce2397c799bedb757bfd3010af/Screen_Shot_2016-10-14_at_18.16.39.png) ## Does this MR meet the acceptance criteria? - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [ ] Added for this feature/bug - [ ] All builds are passing - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Branch has no merge conflicts with `master` (if it does - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #23325 See merge request !6895 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 5 ++--- app/assets/javascripts/dispatcher.js.es6 | 3 +++ app/assets/javascripts/merge_request_tabs.js | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 147b9bdd32d..f0effcc504d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. -## 8.14.0 (2016-11-22) - - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - - Simpler arguments passed to named_route on toggle_award_url helper method +## 8.13.1 (unreleased) + - Fixed hidden pipeline graph on commit and MR page. !6895 ## 8.13.0 (2016-10-22) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index afc0d6f8c62..a1fe57562fa 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -117,6 +117,9 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commit:builds': + new gl.Pipelines(); + break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 9f28738e06b..3dde979185b 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -282,6 +282,7 @@ document.querySelector("div#builds").innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); _this.buildsLoaded = true; + if (!this.pipelines) this.pipelines = new gl.Pipelines(); return _this.scrollToElement("#builds"); }; })(this) -- GitLab From 7d4f2c98cb3867fc95c94c0e700a35a131a71656 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Sat, 22 Oct 2016 09:39:55 +0000 Subject: [PATCH 066/109] Merge branch '22892-cycle-analytics-date-filter-is-not-working' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve "Cycle analytics date filter is not working" Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/22892 See merge request !6906 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 2 ++ app/assets/javascripts/cycle_analytics.js.es6 | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0effcc504d..9ffdf9ce8ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.1 (unreleased) + - Fixed hidden pipeline graph on commit and MR page. !6895 + - Fix Cycle analytics not showing correct data when filtering by date. !6906 ## 8.13.0 (2016-10-22) diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index bd9accacb8c..20791bab942 100644 --- a/app/assets/javascripts/cycle_analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 @@ -36,7 +36,11 @@ method: 'GET', dataType: 'json', contentType: 'application/json', - data: { start_date: options.startDate } + data: { + cycle_analytics: { + start_date: options.startDate + } + } }).done((data) => { this.decorateData(data); this.initDropdown(); -- GitLab From de16bab092d0410ab0d49e783a125d3ec699ac10 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 22 Oct 2016 11:29:03 +0000 Subject: [PATCH 067/109] Merge branch 'ldap-login-styles' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support LDAP login tabs wrapping to second line (and a few other login fixes) Fixes some regressions introduced for customers with multiple servers configured for login and/or long label names. Also, improves styling for the login page on small screens. See the bad behavior here: https://gitlab.com/gitlab-org/gitlab-ce/issues/23435#note_17117893 https://gitlab.com/gitlab-org/gitlab-ce/issues/23435#note_17117677 See merge request !6993 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 3 +- app/assets/stylesheets/pages/login.scss | 34 ++++++++++++++++++- .../devise/sessions/two_factor.html.haml | 2 +- app/views/devise/shared/_tabs_ldap.html.haml | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ffdf9ce8ec..cf8807cc457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.1 (unreleased) - - Fixed hidden pipeline graph on commit and MR page. !6895 + - Fix hidden pipeline graph on commit and MR page. !6895 - Fix Cycle analytics not showing correct data when filtering by date. !6906 + - Ensure custom provider tab labels don't break layout. !6993 ## 8.13.0 (2016-10-22) diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index bdb13bee178..9496234c773 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -143,6 +143,7 @@ &:not(.active) { background-color: $gray-light; + border-left: 1px solid $border-color; } a { @@ -170,6 +171,31 @@ } } + // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). + // These styles prevent this from breaking the layout, and only applied when providers are configured. + + .new-session-tabs.custom-provider-tabs { + flex-wrap: wrap; + + li { + min-width: 85px; + flex-basis: auto; + + // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. + // We are making somewhat of an assumption about the configuration here: that users do not have more than + // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any + // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border + // above one of the bottom row elements. If you know a better way, please implement it! + &:nth-child(n+5) { + border-top: 1px solid $border-color; + } + } + + a { + font-size: 16px; + } + } + .form-control { &:active, &:focus { @@ -203,6 +229,7 @@ .login-page { .col-sm-5.pull-right { float: none !important; + margin-bottom: 45px; } } } @@ -244,7 +271,11 @@ } .navless-container { - padding: 65px; // height of footer + bottom padding of email confirmation link + padding: 65px 15px; // height of footer + bottom padding of email confirmation link + + @media (max-width: $screen-xs-max) { + padding: 0 15px 65px; + } } } @@ -263,3 +294,4 @@ bottom: 0; } } + diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 0e865b807c1..fd77cdbee2e 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -10,7 +10,7 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - .form-group + %div = f.label 'Two-Factor Authentication code', name: :otp_attempt = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index a057f126c45..1e957f0935f 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -1,4 +1,4 @@ -%ul.new-session-tabs.nav-links.nav-tabs +%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) } - if crowd_enabled? %li.active = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' -- GitLab From f602a43b9009a566a01c0b2de3cd72a1069f6a4c Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 24 Oct 2016 11:02:27 +0000 Subject: [PATCH 068/109] Merge branch 'issue-boards-user-url' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use root_url for issue boards user link Rather than using `/` as the root path, it now correctly sends the root URL from Rails to prevent errors with installations in subdirectories. Closes #23556 See merge request !7018 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/views/projects/boards/components/_card.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8807cc457..d4806fe5fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix hidden pipeline graph on commit and MR page. !6895 - Fix Cycle analytics not showing correct data when filtering by date. !6906 - Ensure custom provider tab labels don't break layout. !6993 + - Fix issue boards user link when in subdirectory. !7018 ## 8.13.0 (2016-10-22) diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index d8f16022407..c6d718a1cd1 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -26,7 +26,7 @@ ":title" => "label.description", data: { container: 'body' } } {{ label.title }} - %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", + %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", ":title" => "'Assigned to ' + issue.assignee.name", "v-if" => "issue.assignee", data: { container: 'body' } } -- GitLab From 7c71ebfe521d660a18b286686eee544d4f239007 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 22 Oct 2016 09:22:28 +0000 Subject: [PATCH 069/109] Merge branch 'docs/dynamic-envs-yaml' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor and add new environment functionality to CI yaml reference Add new `environment` functionality to the yaml reference. Part of https://gitlab.com/gitlab-org/gitlab-ce/issues/23484 See merge request !7026 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + doc/ci/yaml/README.md | 160 +++++++++++++++++++++++++++++++++++------- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4806fe5fc5..f2efe02f677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix Cycle analytics not showing correct data when filtering by date. !6906 - Ensure custom provider tab labels don't break layout. !6993 - Fix issue boards user link when in subdirectory. !7018 + - Refactor and add new environment functionality to CI yaml reference. !7026 ## 8.13.0 (2016-10-22) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 84ea59ab687..5c0e1c44e3f 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -146,13 +146,17 @@ variables: ``` These variables can be later used in all executed commands and scripts. - The YAML-defined variables are also set to all created service containers, -thus allowing to fine tune them. +thus allowing to fine tune them. Variables can be also defined on a +[job level](#job-variables). -Variables can be also defined on [job level](#job-variables). +Except for the user defined variables, there are also the ones set up by the +Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of +the branch or tag name for which project is built. Apart from the variables +you can set in `.gitlab-ci.yml`, there are also the so called secret variables +which can be set in GitLab's UI. -[Learn more about variables.](../variables/README.md) +[Learn more about variables.][variables] ### cache @@ -541,20 +545,29 @@ An example usage of manual actions is deployment to production. > Introduced in GitLab 8.9. -`environment` is used to define that a job deploys to a specific [environment]. -This allows easy tracking of all deployments to your environments straight from -GitLab. +> You can read more about environments and find more examples in the +[documentation about environments][environment]. +`environment` is used to define that a job deploys to a specific environment. If `environment` is specified and no environment under that name exists, a new one will be created automatically. -The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common -names are `qa`, `staging`, and `production`, but you can use whatever name works -with your workflow. +The `environment` name can contain: ---- +- letters +- digits +- spaces +- `-` +- `_` +- `/` +- `$` +- `{` +- `}` -**Example configurations** +Common names are `qa`, `staging`, and `production`, but you can use whatever +name works with your workflow. + +In its simplest form, the `environment` keyword can be defined like: ``` deploy to production: @@ -563,39 +576,134 @@ deploy to production: environment: production ``` -The `deploy to production` job will be marked as doing deployment to -`production` environment. +In the above example, the `deploy to production` job will be marked as doing a +deployment to the `production` environment. + +#### environment:name + +> Introduced in GitLab 8.11. + +>**Note:** +Before GitLab 8.11, the name of an environment could be defined as a string like +`environment: production`. The recommended way now is to define it under the +`name` keyword. + +Instead of defining the name of the environment right after the `environment` +keyword, it is also possible to define it as a separate value. For that, use +the `name` keyword under `environment`: + +``` +deploy to production: + stage: deploy + script: git push production HEAD:master + environment: + name: production +``` + +#### environment:url + +> Introduced in GitLab 8.11. + +>**Note:** +Before GitLab 8.11, the URL could be added only in GitLab's UI. The +recommended way now is to define it in `.gitlab-ci.yml`. + +This is an optional value that when set, it exposes buttons in various places +in GitLab which when clicked take you to the defined URL. + +In the example below, if the job finishes successfully, it will create buttons +in the merge requests and in the environments/deployments pages which will point +to `https://prod.example.com`. + +``` +deploy to production: + stage: deploy + script: git push production HEAD:master + environment: + name: production + url: https://prod.example.com +``` + +#### environment:on_stop + +> [Introduced][ce-6669] in GitLab 8.13. + +Closing (stoping) environments can be achieved with the `on_stop` keyword defined under +`environment`. It declares a different job that runs in order to close +the environment. + +Read the `environment:action` section for an example. + +#### environment:action + +> [Introduced][ce-6669] in GitLab 8.13. + +The `action` keyword is to be used in conjunction with `on_stop` and is defined +in the job that is called to close the environment. + +Take for instance: + +```yaml +review_app: + stage: deploy + script: make deploy-app + environment: + name: review + on_stop: stop_review_app + +stop_review_app: + stage: deploy + script: make delete-app + when: manual + environment: + name: review + action: stop +``` + +In the above example we set up the `review_app` job to deploy to the `review` +environment, and we also defined a new `stop_review_app` job under `on_stop`. +Once the `review_app` job is successfully finished, it will trigger the +`stop_review_app` job based on what is defined under `when`. In this case we +set it up to `manual` so it will need a [manual action](#manual-actions) via +GitLab's web interface in order to run. + +The `stop_review_app` job is **required** to have the following keywords defined: + +- `when` - [reference](#when) +- `environment:name` +- `environment:action` #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. `environment` can also represent a configuration hash with `name` and `url`. -These parameters can use any of the defined CI [variables](#variables) +These parameters can use any of the defined [CI variables](#variables) (including predefined, secure variables and `.gitlab-ci.yml` variables). -The common use case is to create dynamic environments for branches and use them -as review apps. - ---- - -**Example configurations** +For example: ``` deploy as review app: stage: deploy - script: ... + script: make deploy environment: name: review-apps/$CI_BUILD_REF_NAME url: https://$CI_BUILD_REF_NAME.review.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically -create the `review-apps/branch-name` environment. +create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME` +is an [environment variable][variables] set by the Runner. If for example the +`deploy as review app` job was run in a branch named `pow`, this environment +should be accessible under `https://pow.review.example.com/`. -This environment should be accessible under `https://branch-name.review.example.com/`. +This of course implies that the underlying server which hosts the application +is properly configured. -You can see a simple example at https://gitlab.com/gitlab-examples/review-apps-nginx/. +The common use case is to create dynamic environments for branches and use them +as Review Apps. You can see a simple example using Review Apps at +https://gitlab.com/gitlab-examples/review-apps-nginx/. ### artifacts @@ -1105,3 +1213,5 @@ CI with various languages. [examples]: ../examples/README.md [ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 [environment]: ../environments.md +[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 +[variables]: ../variables/README.md -- GitLab From 34c652e55340dfa65bc07a6f2b9cc2e3401a0a7c Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 24 Oct 2016 17:06:57 +0000 Subject: [PATCH 070/109] Merge branch 'fix-container-registry-project-settings' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #23575 See merge request !7037 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/views/projects/edit.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2efe02f677..5723c358618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Ensure custom provider tab labels don't break layout. !6993 - Fix issue boards user link when in subdirectory. !7018 - Refactor and add new environment functionality to CI yaml reference. !7026 + - Fix typo in project settings that prevents users from enabling container registry. !7037 ## 8.13.0 (2016-10-22) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fb776e3a3e7..c36a3f00728 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -101,7 +101,7 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - - if Gitlab.config.lfs.enabled && current_user.admin? + - if Gitlab.config.registry.enabled .form-group .checkbox = f.label :container_registry_enabled do -- GitLab From 5f2d097f265b874162c2474afec53920f2d27c8d Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 24 Oct 2016 09:21:21 +0000 Subject: [PATCH 071/109] Merge branch '23557-remove-extra-line-for-empty-issue-description' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This MR removes an odd line for empty description Closes #23557 #23695 See merge request !7045 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/assets/stylesheets/framework/common.scss | 2 ++ app/views/projects/issues/show.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5723c358618..f4d3a6c1377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix issue boards user link when in subdirectory. !7018 - Refactor and add new environment functionality to CI yaml reference. !7026 - Fix typo in project settings that prevents users from enabling container registry. !7037 + - Remove extra line for empty issue description. !7045 ## 8.13.0 (2016-10-22) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 81e4e264560..800e2dba018 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -372,3 +372,5 @@ table { margin-right: -$gl-padding; border-top: 1px solid $border-color; } + +.hide-bottom-border { border-bottom: none !important; } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6f3f238a436..bd629b5c519 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -53,7 +53,7 @@ .issue-details.issuable-details - .detail-page-description.content-block + .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } %h2.title = markdown_field(@issue, :title) - if @issue.description.present? diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c83818e9199..f9ba77e87b5 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -31,7 +31,7 @@ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete - .detail-page-description.milestone-detail + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) %div -- GitLab From 627eb7d126df0abceadf8328b6647b53ea55f723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Sat, 22 Oct 2016 12:52:02 +0000 Subject: [PATCH 072/109] Merge branch 'sh-fix-broken-label-controller' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix error in generating labels Attempting to generate default set of labels would result in an error: ArgumentError: wrong number of arguments (given 1, expected 0) Closes #23649 See merge request !7055 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + lib/gitlab/issues_labels.rb | 2 +- .../projects/labels_controller_spec.rb | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d3a6c1377..c7f9aeef417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Refactor and add new environment functionality to CI yaml reference. !7026 - Fix typo in project settings that prevents users from enabling container registry. !7037 - Remove extra line for empty issue description. !7045 + - Fix error in generating labels. !7055 ## 8.13.0 (2016-10-22) diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index 01a2c19ab23..dbc759367eb 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -19,7 +19,7 @@ module Gitlab ] labels.each do |params| - ::Labels::FindOrCreateService.new(project.owner, project).execute(params) + ::Labels::FindOrCreateService.new(project.owner, project, params).execute end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 622ab154493..41df63d445a 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -70,4 +70,19 @@ describe Projects::LabelsController do get :index, namespace_id: project.namespace.to_param, project_id: project.to_param end end + + describe 'POST #generate' do + let(:admin) { create(:admin) } + let(:project) { create(:empty_project) } + + before do + sign_in(admin) + end + + it 'creates labels' do + post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to have_http_status(302) + end + end end -- GitLab From 01bb7673ebb342d823dab7df0f56c7d49b127020 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Sat, 22 Oct 2016 12:44:53 +0000 Subject: [PATCH 073/109] Merge branch '23653-dont-clear-db-cache-every-release' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop clearing the database cache on rake cache:clear See merge request !7056 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + lib/tasks/cache.rake | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f9aeef417..85cb85b772c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix typo in project settings that prevents users from enabling container registry. !7037 - Remove extra line for empty issue description. !7045 - Fix error in generating labels. !7055 + - Stop clearing the database cache on `rake cache:clear`. !7056 ## 8.13.0 (2016-10-22) diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index a95a3455a4a..78ae187817a 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -29,5 +29,5 @@ namespace :cache do task all: [:db, :redis] end - task clear: 'cache:clear:all' + task clear: 'cache:clear:redis' end -- GitLab From 67df786d4d627dfacc174c8b668885ce8ff14b95 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 25 Oct 2016 02:47:27 +0000 Subject: [PATCH 074/109] Merge branch 'register-tab' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only show register tab if signup enabled. Fixes a regression where the register tab is activated, even if sign-up enabled is not activated in application_settings. https://gitlab.com/gitlab-org/gitlab-ce/issues/23654 See merge request !7058 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + .../devise/shared/_tabs_normal.html.haml | 5 +- spec/features/login_spec.rb | 65 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85cb85b772c..453e77fb840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Remove extra line for empty issue description. !7045 - Fix error in generating labels. !7055 - Stop clearing the database cache on `rake cache:clear`. !7056 + - Only show register tab if signup enabled. !7058 ## 8.13.0 (2016-10-22) diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 79b1d447a92..05246303fb6 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,5 +1,6 @@ %ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} %li.active{ role: 'presentation' } %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in - %li{ role: 'presentation'} - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register + - if signin_enabled? && signup_enabled? + %li{ role: 'presentation'} + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 996f39ea06d..76bcfbe523a 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -215,4 +215,69 @@ feature 'Login', feature: true do end end end + + describe 'UI tabs and panes' do + context 'when no defaults are changed' do + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness + end + end + + context 'when signup is disabled' do + before do + stub_application_setting(signup_enabled: false) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness + end + end + + context 'when ldap is enabled' do + before do + visit new_user_session_path + allow(page).to receive(:form_based_providers).and_return([:ldapmain]) + allow(page).to receive(:ldap_enabled).and_return(true) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness(false) + end + end + + context 'when crowd is enabled' do + before do + visit new_user_session_path + allow(page).to receive(:form_based_providers).and_return([:crowd]) + allow(page).to receive(:crowd_enabled?).and_return(true) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness(false) + end + end + + def ensure_tab_pane_correctness(visit_path = true) + if visit_path + visit new_user_session_path + end + + ensure_tab_pane_counts + ensure_one_active_tab + ensure_one_active_pane + end + + def ensure_tab_pane_counts + tabs_count = page.all('[role="tab"]').size + expect(page).to have_selector('[role="tabpanel"]', count: tabs_count) + end + + def ensure_one_active_tab + expect(page).to have_selector('.nav-tabs > li.active', count: 1) + end + + def ensure_one_active_pane + expect(page).to have_selector('.tab-pane.active', count: 1) + end + end end -- GitLab From 1e94405c648cdf50333e407ac4be0a51798419d5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 24 Oct 2016 12:00:08 +0200 Subject: [PATCH 075/109] Merge remote-tracking branch 'origin/sh-flush-cache-after-import' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See merge request !7064 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/models/repository.rb | 27 +++++++++++++++------------ spec/models/repository_spec.rb | 21 +++++---------------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 453e77fb840..7278f795bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix error in generating labels. !7055 - Stop clearing the database cache on `rake cache:clear`. !7056 - Only show register tab if signup enabled. !7058 + - Expire and build repository cache after project import. !7064 ## 8.13.0 (2016-10-22) diff --git a/app/models/repository.rb b/app/models/repository.rb index 72e473871fa..b6653d18530 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -416,6 +416,17 @@ class Repository @exists = nil end + # expire cache that doesn't depend on repository data (when expiring) + def expire_content_cache + expire_tags_cache + expire_tag_count_cache + expire_branches_cache + expire_branch_count_cache + expire_root_ref_cache + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been created. def after_create expire_exists_cache @@ -431,14 +442,7 @@ class Repository expire_cache if exists? - # expire cache that don't depend on repository data (when expiring) - expire_tags_cache - expire_tag_count_cache - expire_branches_cache - expire_branch_count_cache - expire_root_ref_cache - expire_emptiness_caches - expire_exists_cache + expire_content_cache repository_event(:remove_repository) end @@ -470,14 +474,13 @@ class Repository end def before_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache end # Runs code after a repository has been forked/imported. def after_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache + build_cache end # Runs code after a new commit has been pushed. diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f977cf73673..187a1bf2d79 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1146,28 +1146,17 @@ describe Repository, models: true do end describe '#before_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.before_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes the repository caches' do + expect(repository).to receive(:expire_content_cache) repository.before_import end end describe '#after_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.after_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes and builds the cache' do + expect(repository).to receive(:expire_content_cache) + expect(repository).to receive(:build_cache) repository.after_import end -- GitLab From 9e5f1e1e61f032804a78e5a43b5100106f1e35b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 25 Oct 2016 08:28:07 +0000 Subject: [PATCH 076/109] Merge branch 'sh-fix-labels-move-issue' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix bug where labels would be assigned to issues that were moved If you attempt to move an issue from one project to another and leave labels blank, LabelsFinder would assign all labels in the new project to that issue. The issue is that :title is passed along to the Finder, but since it appears empty no filtering is done. As a result, all labels in the group are returned. This fix handles that case. Closes #23668 See merge request !7065 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/finders/labels_finder.rb | 8 +++--- spec/finders/labels_finder_spec.rb | 32 +++++++++++++++++++++++ spec/services/issues/move_service_spec.rb | 29 +++++++++++++++++--- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7278f795bb4..75dcc6b87bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Stop clearing the database cache on `rake cache:clear`. !7056 - Only show register tab if signup enabled. !7058 - Expire and build repository cache after project import. !7064 + - Fix bug where labels would be assigned to issues that were moved. !7065 ## 8.13.0 (2016-10-22) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 6ace14a4bb5..95e62cdb02a 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -35,8 +35,10 @@ class LabelsFinder < UnionFinder end def with_title(items) - items = items.where(title: title) if title - items + return items if title.nil? + return items.none if title.blank? + + items.where(title: title) end def group_id @@ -52,7 +54,7 @@ class LabelsFinder < UnionFinder end def title - params[:title].presence || params[:name].presence + params[:title] || params[:name] end def project diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 27acc464ea2..10cfb66ec1c 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -38,6 +38,14 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] end + + it 'returns labels available if nil title is supplied' do + group_2.add_developer(user) + # params[:title] will return `nil` regardless whether it is specified + finder = described_class.new(user, title: nil) + + expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] + end end context 'filtering by group_id' do @@ -64,6 +72,30 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2] end + + it 'returns label with title alias' do + finder = described_class.new(user, name: 'Group Label 2') + + expect(finder.execute).to eq [group_label_2] + end + + it 'returns no labels if empty title is supplied' do + finder = described_class.new(user, title: []) + + expect(finder.execute).to be_empty + end + + it 'returns no labels if blank title is supplied' do + finder = described_class.new(user, title: '') + + expect(finder.execute).to be_empty + end + + it 'returns no labels if empty name is supplied' do + finder = described_class.new(user, name: []) + + expect(finder.execute).to be_empty + end end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 93bf0f64963..302eef8bf7e 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do old_project.team << [user, :reporter] new_project.team << [user, :reporter] - ['label1', 'label2'].each do |label| + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| old_issue.labels << create(:label, project_id: old_project.id, title: label) - end - new_project.labels << create(:label, title: 'label1') - new_project.labels << create(:label, title: 'label2') + new_project.labels << create(:label, title: label) + end end end @@ -277,5 +278,25 @@ describe Issues::MoveService, services: true do it { expect { move }.to raise_error(StandardError, /permissions/) } end end + + context 'movable issue with no assigned labels' do + before do + old_project.team << [user, :reporter] + new_project.team << [user, :reporter] + + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| + new_project.labels << create(:label, title: label) + end + end + + include_context 'issue move executed' + + it 'does not assign labels to new issue' do + expected_label_titles = new_issue.reload.labels.map(&:title) + expect(expected_label_titles.size).to eq 0 + end + end end end -- GitLab From 8742a18ea2329c0468f0998b5b869fbd5545e4fb Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 24 Oct 2016 09:50:13 +0000 Subject: [PATCH 077/109] Merge branch 'sh-fix-mailroom-config' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix reply-by-email not working due to queue name mismatch See merge request !7068 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + config/mail_room.yml | 2 +- ...317_migrate_mailroom_queue_from_default.rb | 63 +++++++++++++++++++ db/schema.rb | 2 +- 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 75dcc6b87bf..b2210fcea43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Only show register tab if signup enabled. !7058 - Expire and build repository cache after project import. !7064 - Fix bug where labels would be assigned to issues that were moved. !7065 + - Fix reply-by-email not working due to queue name mismatch. !7068 ## 8.13.0 (2016-10-22) diff --git a/config/mail_room.yml b/config/mail_room.yml index c639f8260aa..68697bd1dc4 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -25,7 +25,7 @@ :delivery_options: :redis_url: <%= config[:redis_url].to_json %> :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> - :queue: incoming_email + :queue: email_receiver :worker: EmailReceiverWorker :arbitration_method: redis diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb new file mode 100644 index 00000000000..06d07bdb835 --- /dev/null +++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb @@ -0,0 +1,63 @@ +require 'json' + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateMailroomQueueFromDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + + DOWNTIME_REASON = <<-EOF + Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping + Sidekiq will result in the loss of jobs that are scheduled after this + migration completes. + EOF + + disable_ddl_transaction! + + # Jobs for which the queue names have been changed (e.g. multiple workers + # using the same non-default queue). + # + # The keys are the old queue names, the values the jobs to move and their new + # queue names. + RENAMED_QUEUES = { + incoming_email: { + 'EmailReceiverWorker' => :email_receiver + } + } + + def up + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |queue, jobs| + migrate_from_queue(redis, queue, jobs) + end + end + end + + def down + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |dest_queue, jobs| + jobs.each do |worker, from_queue| + migrate_from_queue(redis, from_queue, worker => dest_queue) + end + end + end + end + + def migrate_from_queue(redis, queue, job_mapping) + while job = redis.lpop("queue:#{queue}") + payload = JSON.load(job) + new_queue = job_mapping[payload['class']] + + # If we have no target queue to migrate to we're probably dealing with + # some ancient job for which the worker no longer exists. In that case + # there's no sane option we can take, other than just dropping the job. + next unless new_queue + + payload['queue'] = new_queue + + redis.lpush("queue:#{new_queue}", JSON.dump(payload)) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f5c01511195..02282b0f666 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161019213545) do +ActiveRecord::Schema.define(version: 20161024042317) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- GitLab From 6d84f057d451026904fe257f9571178e674ade5a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 24 Oct 2016 12:42:43 +0000 Subject: [PATCH 078/109] Merge branch 'dz-fix-constrainer-for-relative-url' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix constrainers for relative url Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/23675 See merge request !7071 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + lib/constraints/namespace_url_constrainer.rb | 13 ++++++++++++- .../constraints/namespace_url_constrainer_spec.rb | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2210fcea43..2eb914ef181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Expire and build repository cache after project import. !7064 - Fix bug where labels would be assigned to issues that were moved. !7065 - Fix reply-by-email not working due to queue name mismatch. !7068 + - Fix 404 for group pages when GitLab setup uses relative url. !7071 ## 8.13.0 (2016-10-22) diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb index 23920193743..91b70143f11 100644 --- a/lib/constraints/namespace_url_constrainer.rb +++ b/lib/constraints/namespace_url_constrainer.rb @@ -1,6 +1,9 @@ class NamespaceUrlConstrainer def matches?(request) - id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '') + id = request.path + id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root + id = id.sub(/\A\/+/, '').split('/').first + id = id.sub(/.atom\z/, '') if id if id =~ Gitlab::Regex.namespace_regex find_resource(id) @@ -10,4 +13,12 @@ class NamespaceUrlConstrainer def find_resource(id) Namespace.find_by_path(id) end + + private + + def relative_url_root + if defined?(Gitlab::Application.config.relative_url_root) + Gitlab::Application.config.relative_url_root + end + end end diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb index a5feaacb8ee..7814711fe27 100644 --- a/spec/lib/constraints/namespace_url_constrainer_spec.rb +++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb @@ -17,6 +17,16 @@ describe NamespaceUrlConstrainer, lib: true do it { expect(subject.matches?(request '/g/gitlab')).to be_falsey } it { expect(subject.matches?(request '/.gitlab')).to be_falsey } end + + context 'relative url' do + before do + allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } + end + + it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy } + it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey } + it { expect(subject.matches?(request '/gitlab/')).to be_falsey } + end end def request(path) -- GitLab From e6d6c41546a41ca6f1bf16aad89f2068350f9c63 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 25 Oct 2016 10:54:48 +0000 Subject: [PATCH 079/109] Merge branch '23662-issue-move-user-reference-exception' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix `User#to_reference` Fix the method signature of `User#to_reference` so that moving an issue with a user reference does not throw a "invalid number of arguments" exception. 1. Changes in 8.13 require `Referable`s that don't have a project reference to accept two arguments - `from_project` and `target_project`. 2. `User#to_reference` was not changed to accept the `target_project` (even though it is not used). Moving an issue containing a user reference would throw a "invalid number of arguments" exception. 3. The regression was introduced in [c8b2b3f7](https://gitlab.com/gitlab-org/gitlab-ce/commit/c8b2b3f7c32db873f1bebce3e3b1847ea24d235f#91fabb7ad88bd2fde6fef1c100a719c00e503047_75_79), which expects all `Referable`s that don't respond to `:project` to have a `to_reference` method that takes two arguments. Closes #23662 See merge request !7088 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 3 ++- app/models/user.rb | 2 +- spec/services/issues/move_service_spec.rb | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb914ef181..ae1e95c4c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. -## 8.13.1 (unreleased) +## 8.13.1 (2016-10-25) - Fix hidden pipeline graph on commit and MR page. !6895 - Fix Cycle analytics not showing correct data when filtering by date. !6906 @@ -16,6 +16,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix bug where labels would be assigned to issues that were moved. !7065 - Fix reply-by-email not working due to queue name mismatch. !7068 - Fix 404 for group pages when GitLab setup uses relative url. !7071 + - Fix `User#to_reference`. !7088 ## 8.13.0 (2016-10-22) diff --git a/app/models/user.rb b/app/models/user.rb index f367f4616fb..9181db40eb4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -309,7 +309,7 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, _target_project = nil) "#{self.class.reference_prefix}#{username}" end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 302eef8bf7e..f0ded06b785 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -208,10 +208,10 @@ describe Issues::MoveService, services: true do end end - describe 'rewritting references' do + describe 'rewriting references' do include_context 'issue move executed' - context 'issue reference' do + context 'issue references' do let(:another_issue) { create(:issue, project: old_project) } let(:description) { "Some description #{another_issue.to_reference}" } @@ -220,6 +220,16 @@ describe Issues::MoveService, services: true do .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" end end + + context "user references" do + let(:another_issue) { create(:issue, project: old_project) } + let(:description) { "Some description #{user.to_reference}" } + + it "doesn't throw any errors for issues containing user references" do + expect(new_issue.description) + .to eq "Some description #{user.to_reference}" + end + end end context 'moving to same project' do -- GitLab From bfe94698a7e82bf4a5976c8341e28cfcc90bf1de Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 25 Oct 2016 13:21:31 +0000 Subject: [PATCH 080/109] Merge branch 'board-dragging-disabled' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop unauthorized users dragging on issue boards Closes #23763 See merge request !7096 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/helpers/boards_helper.rb | 2 +- spec/features/boards/boards_spec.rb | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1e95c4c6f..6d915a61bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix reply-by-email not working due to queue name mismatch. !7068 - Fix 404 for group pages when GitLab setup uses relative url. !7071 - Fix `User#to_reference`. !7088 + - Fix unauthorized users dragging on issue boards. !7096 ## 8.13.0 (2016-10-22) diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index b7247ffa8b2..38c586ccd31 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -5,7 +5,7 @@ module BoardsHelper { endpoint: namespace_project_boards_path(@project.namespace, @project), board_id: board.id, - disabled: !can?(current_user, :admin_list, @project), + disabled: "#{!can?(current_user, :admin_list, @project)}", issue_link_base: namespace_project_issues_path(@project.namespace, @project) } end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 0fb1608a0a3..c533ce1d87f 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -624,6 +624,10 @@ describe 'Issue Boards', feature: true, js: true do it 'does not show create new list' do expect(page).not_to have_selector('.js-new-board-list') end + + it 'does not allow dragging' do + expect(page).not_to have_selector('.user-can-drag') + end end context 'as guest user' do -- GitLab From 7d08c7fd1e144cc31aeae12d49f8074e5cfa9ab1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 24 Oct 2016 12:55:02 +0000 Subject: [PATCH 081/109] Merge branch '21513-fix-branch-protection-api' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix branch protection API. - Fixes the branch protection API. - Closes #21513 - EE Merge Request: gitlab-org/gitlab-ee!718 See merge request !6215 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + .../concerns/protected_branch_access.rb | 5 + .../protected_branches/api_create_service.rb | 29 +++ .../protected_branches/api_update_service.rb | 47 +++++ lib/api/branches.rb | 48 ++--- spec/requests/api/branches_spec.rb | 190 ++++++++++++------ 6 files changed, 220 insertions(+), 100 deletions(-) create mode 100644 app/services/protected_branches/api_create_service.rb create mode 100644 app/services/protected_branches/api_update_service.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d915a61bbd..70f3478df1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.1 (2016-10-25) + - Fix branch protection API. !6215 - Fix hidden pipeline graph on commit and MR page. !6895 - Fix Cycle analytics not showing correct data when filtering by date. !6906 - Ensure custom provider tab labels don't break layout. !6993 diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 5a7b36070e7..7fd0905ee81 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -1,6 +1,11 @@ module ProtectedBranchAccess extend ActiveSupport::Concern + included do + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + def humanize self.class.human_access_levels[self.access_level] end diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb new file mode 100644 index 00000000000..f2040dfa03a --- /dev/null +++ b/app/services/protected_branches/api_create_service.rb @@ -0,0 +1,29 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiCreateService < BaseService + def execute + push_access_level = + if params.delete(:developers_can_push) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + merge_access_level = + if params.delete(:developers_can_merge) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }]) + + service = ProtectedBranches::CreateService.new(@project, @current_user, @params) + service.execute + end + end +end diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb new file mode 100644 index 00000000000..050cb3b738b --- /dev/null +++ b/app/services/protected_branches/api_update_service.rb @@ -0,0 +1,47 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiUpdateService < BaseService + def execute(protected_branch) + @developers_can_push = params.delete(:developers_can_push) + @developers_can_merge = params.delete(:developers_can_merge) + + @protected_branch = protected_branch + + protected_branch.transaction do + delete_redundant_access_levels + + case @developers_can_push + when true + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + case @developers_can_merge + when true + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) + service.execute(protected_branch) + end + end + + private + + def delete_redundant_access_levels + unless @developers_can_merge.nil? + @protected_branch.merge_access_levels.destroy_all + end + + unless @developers_can_push.nil? + @protected_branch.push_access_levels.destroy_all + end + end + end +end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index b615703df93..6d827448994 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -54,43 +54,25 @@ module API not_found!('Branch') unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) - developers_can_merge = to_boolean(params[:developers_can_merge]) - developers_can_push = to_boolean(params[:developers_can_push]) - protected_branch_params = { - name: @branch.name + name: @branch.name, + developers_can_push: to_boolean(params[:developers_can_push]), + developers_can_merge: to_boolean(params[:developers_can_merge]) } - # If `developers_can_merge` is switched off, _all_ `DEVELOPER` - # merge_access_levels need to be deleted. - if developers_can_merge == false - protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all - end + service_args = [user_project, current_user, protected_branch_params] - # If `developers_can_push` is switched off, _all_ `DEVELOPER` - # push_access_levels need to be deleted. - if developers_can_push == false - protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all - end + protected_branch = if protected_branch + ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch) + else + ProtectedBranches::ApiCreateService.new(*service_args).execute + end - protected_branch_params.merge!( - merge_access_levels_attributes: [{ - access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }], - push_access_levels_attributes: [{ - access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }] - ) - - if protected_branch - service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params) - service.execute(protected_branch) + if protected_branch.valid? + present @branch, with: Entities::RepoBranch, project: user_project else - service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) - service.execute + render_api_error!(protected_branch.errors.full_messages, 422) end - - present @branch, with: Entities::RepoBranch, project: user_project end # Unprotect a single branch @@ -123,7 +105,7 @@ module API post ":id/repository/branches" do authorize_push_project result = CreateBranchService.new(user_project, current_user). - execute(params[:branch_name], params[:ref]) + execute(params[:branch_name], params[:ref]) if result[:status] == :success present result[:branch], @@ -142,10 +124,10 @@ module API # Example Request: # DELETE /projects/:id/repository/branches/:branch delete ":id/repository/branches/:branch", - requirements: { branch: /.+/ } do + requirements: { branch: /.+/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). - execute(params[:branch]) + execute(params[:branch]) if result[:status] == :success { diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 3fd989dd7a6..905f762d578 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -48,92 +48,154 @@ describe API::API, api: true do end describe 'PUT /projects/:id/repository/branches/:branch/protect' do - it 'protects a single branch' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + context "when a protected branch doesn't already exist" do + it 'protects a single branch' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end - it 'protects a single branch and developers can push' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true + it 'protects a single branch and developers can push' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(false) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end - it 'protects a single branch and developers can merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_merge: true + it 'protects a single branch and developers can merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(true) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(true) + end - it 'protects a single branch and developers can push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true, developers_can_merge: true + it 'protects a single branch and developers can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true, developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(true) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end - it 'protects a single branch and developers cannot push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: 'tru', developers_can_merge: 'tr' + it 'protects a single branch and developers cannot push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: 'tru', developers_can_merge: 'tr' - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end end - context 'on a protected branch' do - let(:protected_branch) { 'foo' } - + context 'for an existing protected branch' do before do - project.repository.add_branch(user, protected_branch, 'master') - create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) + project.repository.add_branch(user, protected_branch.name, 'master') end - it 'updates that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: false, developers_can_merge: false + context "when developers can push and merge" do + let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + + it 'updates that a developer cannot push or merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false, developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.push_access_levels.first).to be_present + expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + + it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.merge_access_levels.first).to be_present + expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + end + + context "when developers cannot push or merge" do + let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } + + it 'updates that a developer can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + end + end + + context "multiple API calls" do + it "returns success when `protect` is called twice" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(false) end - it 'does not update that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: 'foobar', developers_can_merge: 'foo' + it "returns success when `protect` is called twice with `developers_can_push` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "returns success when `protect` is called twice with `developers_can_merge` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(true) end end @@ -147,12 +209,6 @@ describe API::API, api: true do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) expect(response).to have_http_status(403) end - - it "returns success when protect branch again" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - expect(response).to have_http_status(200) - end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do -- GitLab From 69ff29407df3a66c2902308e442451b8225303e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 25 Oct 2016 15:49:44 +0000 Subject: [PATCH 082/109] Merge branch 'patch-7' into 'master' This will change the update process to checkout gitlab-shell version 3.6.6 instead of 3.6.3 as currently described in the update document See merge request !6976 --- doc/update/8.12-to-8.13.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index 8940d14559b..c0084d9d59c 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.6.3 +sudo -u git -H git checkout v3.6.6 ``` ### 6. Update gitlab-workhorse -- GitLab From 0778461d7960b9d159b04a59adc93c66ee67f218 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 25 Oct 2016 15:28:59 +0000 Subject: [PATCH 083/109] Merge branch 'temporarily-revert-appending-templates-before-long-term-ux-fix' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop appending templates - Temporary fix This is the temporary fix for #23315. This stops the templates appending to any existing text. See merge request !7050 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/assets/javascripts/blob/template_selector.js.es6 | 7 +------ .../templates/issuable_template_selector.js.es6 | 12 +++++------- spec/features/projects/issuable_templates_spec.rb | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f3478df1b..05686885656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Refactor and add new environment functionality to CI yaml reference. !7026 - Fix typo in project settings that prevents users from enabling container registry. !7037 - Remove extra line for empty issue description. !7045 + - Don't append issue/MR templates to any existing text. !7050 - Fix error in generating labels. !7055 - Stop clearing the database cache on `rake cache:clear`. !7056 - Only show register tab if signup enabled. !7058 diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 4e309e480b0..2d5c6ade053 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -68,14 +68,10 @@ // To be implemented on the extending class // e.g. // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus, append } = {}) { + requestFileSuccess(file, { skipFocus } = {}) { const oldValue = this.editor.getValue(); let newValue = file.content; - if (append && oldValue.length && oldValue !== newValue) { - newValue = oldValue + '\n\n' + newValue; - } - this.editor.setValue(newValue, 1); if (!skipFocus) this.editor.focus(); @@ -99,4 +95,3 @@ global.TemplateSelector = TemplateSelector; })(window.gl || ( window.gl = {})); - diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index bd4e3c3d00d..fa1b79c8415 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -32,24 +32,22 @@ this.currentTemplate = currentTemplate; if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); - this.setInputValueToTemplateContent(true); + this.setInputValueToTemplateContent(); }); return; } - setInputValueToTemplateContent(append) { + setInputValueToTemplateContent() { // `this.requestFileSuccess` sets the value of the description input field - // to the content of the template selected. If `append` is true, the - // template content will be appended to the previous value of the field, - // separated by a blank line if the previous value is non-empty. + // to the content of the template selected. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and // skip focusing the description input by setting `true` as the // `skipFocus` option to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); + this.requestFileSuccess(this.currentTemplate, {skipFocus: true}); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); + this.requestFileSuccess(this.currentTemplate, {skipFocus: false}); } return; } diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index d886909ce85..2f377312ea5 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template("#{prior_description}\n\n#{template_content}") + preview_template("#{template_content}") save_changes end end -- GitLab From 5dea12ac22a808261a610ac156c33ef5c09c2bfd Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 25 Oct 2016 15:55:59 +0000 Subject: [PATCH 084/109] Merge branch 'sh-optimize-label-finder' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce overhead of LabelFinder by avoiding #presence call Some users experienced 502 timeouts when viewing group labels. Labels#open_issues_count and Label#open_merge_requests_count were taking a long time to load because they were loading every ActiveRecord of the user-accessible projects into memory. This change modifies so that only the IDs are loaded into memory. Closes #23684 See merge request !7094 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/finders/labels_finder.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05686885656..53100174fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix reply-by-email not working due to queue name mismatch. !7068 - Fix 404 for group pages when GitLab setup uses relative url. !7071 - Fix `User#to_reference`. !7088 + - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 - Fix unauthorized users dragging on issue boards. !7096 ## 8.13.0 (2016-10-22) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 95e62cdb02a..44484d64567 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -50,7 +50,7 @@ class LabelsFinder < UnionFinder end def projects_ids - params[:project_ids].presence + params[:project_ids] end def title -- GitLab From 1e4b63b7ead156c852034bb23c6e3dc765f04d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 25 Oct 2016 16:26:59 +0000 Subject: [PATCH 085/109] Merge branch 'project-cache-worker-scheduling' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't schedule ProjectCacheWorker unless needed This MR changes `ProjectCacheWorker.perform_async` so scheduling only takes place when needed. See the commits for more details. See merge request !7099 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + app/workers/project_cache_worker.rb | 16 +++++++-- lib/gitlab/exclusive_lease.rb | 9 ++++- spec/lib/gitlab/exclusive_lease_spec.rb | 43 +++++++++++++++-------- spec/spec_helper.rb | 6 ++++ spec/workers/project_cache_worker_spec.rb | 20 +++++++++++ 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53100174fa2..fd720fb8f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix `User#to_reference`. !7088 - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 - Fix unauthorized users dragging on issue boards. !7096 + - Only schedule `ProjectCacheWorker` jobs when needed. !7099 ## 8.13.0 (2016-10-22) diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 71b274e0c99..4dfa745fb50 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -9,6 +9,18 @@ class ProjectCacheWorker LEASE_TIMEOUT = 15.minutes.to_i + def self.lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) + end + + # Overwrite Sidekiq's implementation so we only schedule when actually needed. + def self.perform_async(project_id) + # If a lease for this project is still being held there's no point in + # scheduling a new job. + super unless lease_for(project_id).exists? + end + def perform(project_id) if try_obtain_lease_for(project_id) Rails.logger. @@ -37,8 +49,6 @@ class ProjectCacheWorker end def try_obtain_lease_for(project_id) - Gitlab::ExclusiveLease. - new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT). - try_obtain + self.class.lease_for(project_id).try_obtain end end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index ffe49364379..7e8f35e9298 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -27,7 +27,7 @@ module Gitlab # on begin/ensure blocks to cancel a lease, because the 'ensure' does # not always run. Think of 'kill -9' from the Unicorn master for # instance. - # + # # If you find that leases are getting in your way, ask yourself: would # it be enough to lower the lease timeout? Another thing that might be # appropriate is to only use a lease for bulk/automated operations, and @@ -48,6 +48,13 @@ module Gitlab end end + # Returns true if the key for this lease is set. + def exists? + Gitlab::Redis.with do |redis| + redis.exists(redis_key) + end + end + # No #cancel method. See comments above! private diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index fbdb7ea34ac..6b3bd08b978 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -1,21 +1,36 @@ require 'spec_helper' -describe Gitlab::ExclusiveLease do - it 'cannot obtain twice before the lease has expired' do - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to eq(true) - expect(lease.try_obtain).to eq(false) - end +describe Gitlab::ExclusiveLease, type: :redis do + let(:unique_key) { SecureRandom.hex(10) } + + describe '#try_obtain' do + it 'cannot obtain twice before the lease has expired' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to eq(true) + expect(lease.try_obtain).to eq(false) + end - it 'can obtain after the lease has expired' do - timeout = 1 - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) - lease.try_obtain # start the lease - sleep(2 * timeout) # lease should have expired now - expect(lease.try_obtain).to eq(true) + it 'can obtain after the lease has expired' do + timeout = 1 + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) + lease.try_obtain # start the lease + sleep(2 * timeout) # lease should have expired now + expect(lease.try_obtain).to eq(true) + end end - def unique_key - SecureRandom.hex(10) + describe '#exists?' do + it 'returns true for an existing lease' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + lease.try_obtain + + expect(lease.exists?).to eq(true) + end + + it 'returns false for a lease that does not exist' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + + expect(lease.exists?).to eq(false) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b19f5824236..06d52f0f735 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,6 +50,12 @@ RSpec.configure do |config| example.run Rails.cache = caching_store end + + config.around(:each, :redis) do |example| + Gitlab::Redis.with(&:flushall) + example.run + Gitlab::Redis.with(&:flushall) + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index f5b60b90d11..bfa8c0ff2c6 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -5,6 +5,26 @@ describe ProjectCacheWorker do subject { described_class.new } + describe '.perform_async' do + it 'schedules the job when no lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(false) + + expect_any_instance_of(described_class).to receive(:perform) + + described_class.perform_async(project.id) + end + + it 'does not schedule the job when a lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(true) + + expect_any_instance_of(described_class).not_to receive(:perform) + + described_class.perform_async(project.id) + end + end + describe '#perform' do context 'when an exclusive lease can be obtained' do before do -- GitLab From dbee13d6190856ed6a5e068af8727cadbbe680de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 25 Oct 2016 16:28:10 +0000 Subject: [PATCH 086/109] Merge branch 'fix-events-api' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix events order in users/:id/events endpoint Order of events in contributions API is currently being lost, though docs are saying: > Get the contribution events for the specified user, sorted **from newest to oldest**. Order becomes different after `.merge(ProjectsFinder.new.execute(current_user))` call, so I moved ordering below this line. This MR also removes extra `.page(params[:page])` call in the method chain, since [`paginate(events)` already does it](https://gitlab.com/airat/gitlab-ce/blob/master/lib/api/helpers.rb#L112). See merge request !7039 Signed-off-by: Rémy Coutable --- CHANGELOG.md | 1 + lib/api/users.rb | 4 ++-- spec/requests/api/users_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd720fb8f34..598f15da6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix issue boards user link when in subdirectory. !7018 - Refactor and add new environment functionality to CI yaml reference. !7026 - Fix typo in project settings that prevents users from enabling container registry. !7037 + - Fix events order in `users/:id/events` endpoint. !7039 - Remove extra line for empty issue description. !7045 - Don't append issue/MR templates to any existing text. !7050 - Fix error in generating labels. !7055 diff --git a/lib/api/users.rb b/lib/api/users.rb index e868f628404..c28e07a76b7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -333,11 +333,11 @@ module API user = User.find_by(id: declared(params).id) not_found!('User') unless user - events = user.recent_events. + events = user.events. merge(ProjectsFinder.new.execute(current_user)). references(:project). with_associations. - page(params[:page]) + recent present paginate(events), with: Entities::Event end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f83f4d2c9b1..2c4e73ed578 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -958,6 +958,29 @@ describe API::API, api: true do expect(joined_event['author']['name']).to eq(user.name) end end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + let(:third_note) { create(:note_on_issue, project: project) } + + before do + second_note.project.add_user(user, :developer) + + [second_note, third_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + + expect(comment_events[0]['target_id']).to eq(third_note.id) + expect(comment_events[1]['target_id']).to eq(second_note.id) + expect(comment_events[2]['target_id']).to eq(note.id) + end + end end it 'returns a 404 error if not found' do -- GitLab From 91f25c2f6e36ebc9a898317b84742f11e6309241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Tue, 25 Oct 2016 16:53:00 -0300 Subject: [PATCH 087/109] Update VERSION to 8.13.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 35de8148559..f61979f1181 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.0 +8.13.1 -- GitLab From 92ced756dbdd4ba113a84d2ef43c293b2a47bf8b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 26 Oct 2016 14:47:33 +0200 Subject: [PATCH 088/109] Update gitlab-shell version to 3.6.6 [ci skip] --- doc/update/8.12-to-8.13.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index 8940d14559b..c0084d9d59c 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.6.3 +sudo -u git -H git checkout v3.6.6 ``` ### 6. Update gitlab-workhorse -- GitLab From 1bf622c2419d5f86d9ee88e27c46f6509fdd436d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 28 Oct 2016 16:44:35 +0000 Subject: [PATCH 089/109] Merge branch 'ee-1159-allow-permission-check-bypass-in-approve-access-request-service' into 'master' Allow Members::ApproveAccessRequestService to accept a new `:force` option ## What does this MR do? See the commit message. This is a backport of the EE fix for https://gitlab.com/gitlab-org/gitlab-ee/issues/1159: gitlab-org/gitlab-ee!830 See merge request !7168 --- .../members/approve_access_request_service.rb | 21 +++++-- .../approve_access_request_service_spec.rb | 63 +++++++++++++++++-- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index 416aee2ab51..c13f289f61e 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -4,17 +4,25 @@ module Members attr_accessor :source + # source - The source object that respond to `#requesters` (i.g. project or group) + # current_user - The user that performs the access request approval + # params - A hash of parameters + # :user_id - User ID used to retrieve the access requester + # :id - Member ID used to retrieve the access requester + # :access_level - Optional access level set when the request is accepted def initialize(source, current_user, params = {}) @source = source @current_user = current_user - @params = params + @params = params.slice(:user_id, :id, :access_level) end - def execute + # opts - A hash of options + # :force - Bypass permission check: current_user can be nil in that case + def execute(opts = {}) condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } access_requester = source.requesters.find_by!(condition) - raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester) + raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts) access_requester.access_level = params[:access_level] if params[:access_level] access_requester.accept_request @@ -24,8 +32,11 @@ module Members private - def can_update_access_requester?(access_requester) - access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester) + def can_update_access_requester?(access_requester, opts = {}) + access_requester && ( + opts[:force] || + can?(current_user, action_member_permission(:update, access_requester), access_requester) + ) end end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index 03e296259f9..7b090343a3e 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do let(:access_requester) { create(:user) } let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } + let(:opts) { {} } shared_examples 'a service raising ActiveRecord::RecordNotFound' do it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service approving an access request' do it 'succeeds' do - expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1) + expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1) end it 'returns a Member' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params).execute(opts) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_nil end context 'with a custom access level' do - let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } } + let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) } it 'returns a ProjectMember with the custom access level' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params2).execute(opts) expect(member.access_level).to eq Gitlab::Access::MASTER end @@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do end let(:params) { { user_id: access_requester.id } } + context 'when current user is nil' do + let(:user) { nil } + + context 'and :force option is not given' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is false' do + let(:opts) { { force: false } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is true' do + let(:opts) { { force: true } } + + it_behaves_like 'a service approving an access request' do + let(:source) { project } + end + + it_behaves_like 'a service approving an access request' do + let(:source) { group } + end + end + + context 'and :force param is true' do + let(:params) { { user_id: access_requester.id, force: true } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + end + context 'when current user cannot approve access request to the project' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { project } -- GitLab From a7f5927579b00651723fa12c9ed44b80ecf9803a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 28 Oct 2016 17:06:50 +0000 Subject: [PATCH 090/109] Merge branch 'fix-8-13-changelog-gh-import' into 'master' Fix CHANGELOG for GH import fixes See merge request !7173 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 598f15da6ad..d19573670bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ Please view this file on the master branch, on stable branches it's out of date. +## 8.14.0 (2016-11-22) + - Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 + - Adds user project membership expired event to clarify why user was removed (Callum Dryden) + - Trim leading and trailing whitespace on project_path (Linus Thiel) + - Prevent award emoji via notes for issues/MRs authored by user (barthc) + - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) + - Fix extra space on Build sidebar on Firefox !7060 + - Fix mobile layout issues in admin user overview page !7087 + - Fix HipChat notifications rendering (airatshigapov, eisnerd) + - Refactor Jira service to use jira-ruby gem + - Add hover to trash icon in notes !7008 (blackst0ne) + - Only show one error message for an invalid email !5905 (lycoperdon) + - Fix sidekiq stats in admin area (blackst0ne) + - API: Fix booleans not recognized as such when using the `to_boolean` helper + - Removed delete branch tooltip !6954 + - Stop unauthorized users dragging on milestone page (blackst0ne) + - Restore issue boards welcome message when a project is created !6899 + - Escape ref and path for relative links !6050 (winniehell) + - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) + - Fix filtering of milestones with quotes in title (airatshigapov) + - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) + - Update mail_room and enable sentinel support to Reply By Email (!7101) + - Simpler arguments passed to named_route on toggle_award_url helper method + - Fix typo in framework css class. !7086 (Daniel Voogsgerd) + - New issue board list dropdown stays open after adding a new list + - Fix: Backup restore doesn't clear cache + - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) + - Replace jquery.cookie plugin with js.cookie !7085 + - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method + - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens + - Show full status link on MR & commit pipelines + - Fix documents and comments on Build API `scope` + - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) + - Shortened merge request modal to let clipboard button not overlap + +## 8.13.2 + - Fix builds dropdown overlapping bug !7124 + - Fix applying labels for GitHub-imported MRs !7139 + - Fix importing MR comments from GitHub !7139 + - Modify GitHub importer to be retryable !7003 + - Fix and improve `Sortable.highest_label_priority` + ## 8.13.1 (2016-10-25) - Fix branch protection API. !6215 -- GitLab From d31fa260d067bbdb1e3b6c82bbf1c01a5f66f54d Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 28 Oct 2016 22:48:14 +0000 Subject: [PATCH 091/109] Merge branch 'sticky-mr-tabs-pinned-nav' into 'master' Fixed sticky MR tabs positioning when sidebar is pinned ## What does this MR do? The sticky MR tabs where positioned underneath the pinned sidebar. This fixes that by accounting for the size of the pinned nav. ## Screenshots (if relevant) ![Screen_Shot_2016-10-28_at_09.37.18](/uploads/0d23f2bb0e02d698c012c22c8653afd7/Screen_Shot_2016-10-28_at_09.37.18.png) ## What are the relevant issue numbers? Closes #23926 See merge request !7167 --- CHANGELOG.md | 1 + app/assets/stylesheets/framework/sidebar.scss | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19573670bc..6e58658f427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix importing MR comments from GitHub !7139 - Modify GitHub importer to be retryable !7003 - Fix and improve `Sortable.highest_label_priority` + - Fixed sticky merge request tabs when sidebar is pinned ## 8.13.1 (2016-10-25) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 1d8e64a0e4b..c54f7b27575 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -164,6 +164,18 @@ padding-left: $sidebar_width; } } + + .merge-request-tabs-holder.affix { + @media (min-width: $sidebar-breakpoint) { + left: $sidebar_width; + } + } + + &.right-sidebar-expanded { + .line-resolve-all-container { + display: none; + } + } } header.header-sidebar-pinned { -- GitLab From ef037519673c5619319793fbe922849b3ca1369b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 28 Oct 2016 11:50:43 +0000 Subject: [PATCH 092/109] Merge branch '23928-sortable-highest_label_priority-is-bugged' into 'master' Fix and improve `Sortable.highest_label_priority` Closes #23928 See merge request !7165 --- CHANGELOG.md | 1 + app/models/concerns/sortable.rb | 11 +- app/models/todo.rb | 2 +- spec/features/todos/todos_sorting_spec.rb | 126 +++++++++++++--------- spec/models/concerns/issuable_spec.rb | 14 +++ 5 files changed, 102 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e58658f427..19dd803b38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Modify GitHub importer to be retryable !7003 - Fix and improve `Sortable.highest_label_priority` - Fixed sticky merge request tabs when sidebar is pinned + - Fix and improve `Sortable.highest_label_priority` ## 8.13.1 (2016-10-25) diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 12b23f00769..7edb0acd56c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -38,16 +38,21 @@ module Sortable private - def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: []) + def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) query = Label.select(LabelPriority.arel_table[:priority].minimum). left_join_priorities. joins(:label_links). where("label_priorities.project_id = #{project_column}"). - where(label_links: { target_type: target_type }). where("label_links.target_id = #{target_column}"). reorder(nil) - query.where.not(title: excluded_labels) if excluded_labels.present? + if target_type_column + query = query.where("label_links.target_type = #{target_type_column}") + else + query = query.where(label_links: { target_type: target_type }) + end + + query = query.where.not(title: excluded_labels) if excluded_labels.present? query end diff --git a/app/models/todo.rb b/app/models/todo.rb index 11c072dd000..f5ade1cc293 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -53,7 +53,7 @@ class Todo < ActiveRecord::Base # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" def order_by_labels_priority params = { - target_type: ['Issue', 'MergeRequest'], + target_type_column: "todos.target_type", target_column: "todos.target_id", project_column: "todos.project_id" } diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb index e74a51acede..fec28c55d30 100644 --- a/spec/features/todos/todos_sorting_spec.rb +++ b/spec/features/todos/todos_sorting_spec.rb @@ -8,60 +8,90 @@ describe "Dashboard > User sorts todos", feature: true do let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } - let(:issue_1) { create(:issue, title: 'issue_1', project: project) } - let(:issue_2) { create(:issue, title: 'issue_2', project: project) } - let(:issue_3) { create(:issue, title: 'issue_3', project: project) } - let(:issue_4) { create(:issue, title: 'issue_4', project: project) } - - let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } - - before do - create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) - create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago) - create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago) - create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago) - create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) - - merge_request_1.labels << label_1 - issue_3.labels << label_1 - issue_2.labels << label_3 - issue_1.labels << label_2 - - project.team << [user, :developer] - login_as(user) - visit dashboard_todos_path - end + before { project.team << [user, :developer] } - it "sorts with oldest created todos first" do - click_link "Last created" + context 'sort options' do + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let(:issue_3) { create(:issue, title: 'issue_3', project: project) } + let(:issue_4) { create(:issue, title: 'issue_4', project: project) } - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("merge_request_1") - expect(results_list.all('p')[1]).to have_content("issue_1") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") - end + let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } + + before do + create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) + create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago) + create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago) + create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago) + create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + + merge_request_1.labels << label_1 + issue_3.labels << label_1 + issue_2.labels << label_3 + issue_1.labels << label_2 + + login_as(user) + visit dashboard_todos_path + end + + it "sorts with oldest created todos first" do + click_link "Last created" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("merge_request_1") + expect(results_list.all('p')[1]).to have_content("issue_1") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end - it "sorts with newest created todos first" do - click_link "Oldest created" + it "sorts with newest created todos first" do + click_link "Oldest created" - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_4") - expect(results_list.all('p')[1]).to have_content("issue_2") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_1") - expect(results_list.all('p')[4]).to have_content("merge_request_1") + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_4") + expect(results_list.all('p')[1]).to have_content("issue_2") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_1") + expect(results_list.all('p')[4]).to have_content("merge_request_1") + end + + it "sorts by priority" do + click_link "Priority" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_3") + expect(results_list.all('p')[1]).to have_content("merge_request_1") + expect(results_list.all('p')[2]).to have_content("issue_1") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end end - it "sorts by priority" do - click_link "Priority" + context 'issues and merge requests' do + let(:issue_1) { create(:issue, id: 10000, title: 'issue_1', project: project) } + let(:issue_2) { create(:issue, id: 10001, title: 'issue_2', project: project) } + let(:merge_request_1) { create(:merge_request, id: 10000, title: 'merge_request_1', source_project: project) } + + before do + issue_1.labels << label_1 + issue_2.labels << label_2 + + create(:todo, user: user, project: project, target: issue_1) + create(:todo, user: user, project: project, target: issue_2) + create(:todo, user: user, project: project, target: merge_request_1) + + login_as(user) + visit dashboard_todos_path + end + + it "doesn't mix issues and merge requests priorities" do + click_link "Priority" - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_3") - expect(results_list.all('p')[1]).to have_content("merge_request_1") - expect(results_list.all('p')[2]).to have_content("issue_1") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_1") + expect(results_list.all('p')[1]).to have_content("issue_2") + expect(results_list.all('p')[2]).to have_content("merge_request_1") + end end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 60e4bbc8564..a59d30687f6 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -298,6 +298,20 @@ describe Issue, "Issuable" do end end + describe '.order_labels_priority' do + let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) } + + subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority } + + before do + issue.labels << label_1 + issue.labels << label_2 + end + + it { is_expected.to eq(2) } + end + describe ".with_label" do let(:project) { create(:project, :public) } let(:bug) { create(:label, project: project, title: 'bug') } -- GitLab From 932822f464bf8f5f57167e9dd0985079f1cd6551 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 28 Oct 2016 11:42:01 +0000 Subject: [PATCH 093/109] Merge branch '23890-api-should-accepts-boolean' into 'master' API: Fix booleans not recognized as such when using the `to_boolean` helper Fixes #22831 Fixes #23890 See merge request !7149 --- lib/api/helpers.rb | 1 + spec/requests/api/api_helpers_spec.rb | 29 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 45120898b76..8025581d3ca 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -6,6 +6,7 @@ module API SUDO_PARAM = :sudo def to_boolean(value) + return value if [true, false].include?(value) return true if value =~ /^(true|t|yes|y|1|on)$/i return false if value =~ /^(false|f|no|n|0|off)$/i diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 0f41f8dc7f1..f7fe4c10873 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -266,18 +266,25 @@ describe API::Helpers, api: true do end describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + it 'converts a valid string to a boolean' do - expect(to_boolean('true')).to be_truthy - expect(to_boolean('YeS')).to be_truthy - expect(to_boolean('t')).to be_truthy - expect(to_boolean('1')).to be_truthy - expect(to_boolean('ON')).to be_truthy - expect(to_boolean('FaLse')).to be_falsy - expect(to_boolean('F')).to be_falsy - expect(to_boolean('NO')).to be_falsy - expect(to_boolean('n')).to be_falsy - expect(to_boolean('0')).to be_falsy - expect(to_boolean('oFF')).to be_falsy + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean('oFF')).to be(false) end it 'converts an invalid string to nil' do -- GitLab From c4cb9630f64b4aa7dbba33360194e43570b2642c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 27 Oct 2016 10:16:36 +0000 Subject: [PATCH 094/109] Merge branch '23258-invalid-encoding' into 'master' Fix encoding issues on pipeline commits ## What does this MR do? #### What does this MR do? Sets `escape: false` on `truncate` method to fix commit message on pipelines page #### Screenshots (if relevant) Before: ![Screen_Shot_2016-10-12_at_8.53.10_AM](/uploads/5e26e98a272139fe2264c315d579178f/Screen_Shot_2016-10-12_at_8.53.10_AM.png) After: ![Screen_Shot_2016-10-12_at_8.52.49_AM](/uploads/58c6c69f2ba735fdcd5a0b6922b56aa7/Screen_Shot_2016-10-12_at_8.52.49_AM.png) #### What are the relevant issue numbers? Closes #23258 See merge request !6832 --- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index c6f359f5679..d6923562080 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -40,7 +40,7 @@ %p.commit-title - if commit = pipeline.commit = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" + = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch -- GitLab From 4e4f0d69515344efb20880f40c59b23925122824 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 19 Oct 2016 20:21:10 +0000 Subject: [PATCH 095/109] Merge branch 'adam-fix-ruby-2-1-cycle-analytics' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Hash rocket syntax to fix cycle analytics under Ruby 2.1 Refers to #23510 See merge request !6977 Signed-off-by: Rémy Coutable --- app/views/projects/cycle_analytics/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 7f346df8797..b647882efa0 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,10 +2,10 @@ - page_title "Cycle Analytics" = render "projects/pipelines/head" -#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} +#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} - = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") + = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") .row .col-sm-3.col-xs-12.svg-container = custom_icon('icon_cycle_analytics_splash') -- GitLab From 4dc040c5539f154552b4d12893e315b264b2f17c Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 28 Oct 2016 21:46:44 +0000 Subject: [PATCH 096/109] Merge branch '23849-pipeline-graph-bug' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only remove right connector of first build of last stage Closes #23849 See merge request !7179 Signed-off-by: Rémy Coutable --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 5b8dc7f8c40..8ce03df64fa 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -570,7 +570,7 @@ .build { // Remove right connecting horizontal line from first build in last stage &:first-child { - &::after, &::before { + &::after { border: none; } } -- GitLab From 25d77f765e3f319dad3bf85b7ea53d5bef92806f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 28 Oct 2016 08:48:08 +0000 Subject: [PATCH 097/109] Merge branch 'fix/gh-import-bugs' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix couple of GitHub importing bugs Fix a bug in GH comment importing and label applying for imported MRs. See merge request !7139 Signed-off-by: Rémy Coutable --- lib/gitlab/github_import/client.rb | 12 ++++++---- lib/gitlab/github_import/importer.rb | 24 ++++++++++++------- .../lib/gitlab/github_import/importer_spec.rb | 1 + 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 7f424b74efb..348005d5659 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -105,18 +105,20 @@ module Gitlab data = api.send(method, *args) return data unless data.is_a?(Array) + last_response = api.last_response + if block_given? yield data - each_response_page(&block) + # api.last_response could change while we're yielding (e.g. fetching labels for each PR) + # so we cache our own last request + each_response_page(last_response, &block) else - each_response_page { |page| data.concat(page) } + each_response_page(last_response) { |page| data.concat(page) } data end end - def each_response_page - last_response = api.last_response - + def each_response_page(last_response) while last_response.rels[:next] sleep rate_limit_sleep_time if rate_limit_exceed? last_response = last_response.rels[:next].get diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 4b70f33a851..27946dff608 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -132,8 +132,15 @@ module Gitlab end def apply_labels(issuable, raw_issuable) - if raw_issuable.labels.count > 0 - label_ids = raw_issuable.labels + # GH returns labels for issues but not for pull requests! + labels = if issuable.is_a?(MergeRequest) + client.labels_for_issue(repo, raw_issuable.number) + else + raw_issuable.labels + end + + if labels.count > 0 + label_ids = labels .map { |attrs| @labels[attrs.name] } .compact @@ -143,21 +150,22 @@ module Gitlab def import_comments client.issues_comments(repo, per_page: 100) do |comments| - create_comments(comments, :issue) + create_comments(comments) end client.pull_requests_comments(repo, per_page: 100) do |comments| - create_comments(comments, :pull_request) + create_comments(comments) end end - def create_comments(comments, issuable_type) + def create_comments(comments) ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) - issuable_class = issuable_type == :issue ? Issue : MergeRequest - iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly + comment = CommentFormatter.new(project, raw) + # GH does not return info about comment's parent, so we guess it by checking its URL! + *_, parent, iid = URI(raw.html_url).path.split('/') + issuable_class = parent == 'issues' ? Issue : MergeRequest issuable = issuable_class.find_by_iid(iid) next unless issuable diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 1af553f8f03..84f280ceaae 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -154,6 +154,7 @@ describe Gitlab::GithubImport::Importer, lib: true do { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, + { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } -- GitLab From 897696d4cd5c82d2fd32bb94108996e485e8d192 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 28 Oct 2016 12:44:32 +0000 Subject: [PATCH 098/109] Merge branch 'fix/make-github-import-retryable' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify GitHub importer to be retryable Closes #23533 See merge request !7003 Signed-off-by: Rémy Coutable --- app/services/projects/import_service.rb | 2 +- lib/gitlab/github_import/base_formatter.rb | 4 +- lib/gitlab/github_import/client.rb | 2 +- lib/gitlab/github_import/importer.rb | 93 ++++++++++++++++--- lib/gitlab/github_import/issue_formatter.rb | 8 +- lib/gitlab/github_import/label_formatter.rb | 4 +- .../github_import/milestone_formatter.rb | 8 +- .../github_import/pull_request_formatter.rb | 8 +- lib/gitlab/github_import/release_formatter.rb | 8 +- .../lib/gitlab/github_import/importer_spec.rb | 7 +- spec/services/projects/import_service_spec.rb | 2 +- 11 files changed, 119 insertions(+), 27 deletions(-) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index e466ffa60eb..d7221fe993c 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -29,7 +29,7 @@ module Projects if unknown_url? # In this case, we only want to import issues, not a repository. create_repository - else + elsif !project.repository_exists? import_repository end end diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 8cacf4f4925..6dbae64a9fe 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -10,7 +10,9 @@ module Gitlab end def create! - self.klass.create!(self.attributes) + project.public_send(project_association).find_or_create_by!(find_condition) do |record| + record.attributes = attributes + end end private diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 348005d5659..85df6547a67 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -110,7 +110,7 @@ module Gitlab if block_given? yield data # api.last_response could change while we're yielding (e.g. fetching labels for each PR) - # so we cache our own last request + # so we cache our own last response each_response_page(last_response, &block) else each_response_page(last_response) { |page| data.concat(page) } diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 27946dff608..ecc28799737 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -24,7 +24,8 @@ module Gitlab import_milestones import_issues import_pull_requests - import_comments + import_comments(:issues) + import_comments(:pull_requests) import_wiki import_releases handle_errors @@ -48,7 +49,7 @@ module Gitlab end def import_labels - client.labels(repo, per_page: 100) do |labels| + fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin label = LabelFormatter.new(project, raw).create! @@ -61,7 +62,7 @@ module Gitlab end def import_milestones - client.milestones(repo, state: :all, per_page: 100) do |milestones| + fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| milestones.each do |raw| begin MilestoneFormatter.new(project, raw).create! @@ -73,7 +74,7 @@ module Gitlab end def import_issues - client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| + fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| gh_issue = IssueFormatter.new(project, raw) @@ -90,7 +91,7 @@ module Gitlab end def import_pull_requests - client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| + fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| pull_request = PullRequestFormatter.new(project, raw) next unless pull_request.valid? @@ -148,12 +149,23 @@ module Gitlab end end - def import_comments - client.issues_comments(repo, per_page: 100) do |comments| - create_comments(comments) - end + def import_comments(issuable_type) + resource_type = "#{issuable_type}_comments".to_sym + + # Two notes here: + # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note, + # compare it against every comment in the current imported page until we find match, and that's where start importing + # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns + # only comments on diffs, so select last note not based on noteable_type but on line_code + line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL' + last_note = project.notes.where("line_code IS #{line_code_is}").last + + fetch_resources(resource_type, repo, per_page: 100) do |comments| + if last_note + discard_inserted_comments(comments, last_note) + last_note = nil + end - client.pull_requests_comments(repo, per_page: 100) do |comments| create_comments(comments) end end @@ -177,6 +189,24 @@ module Gitlab end end + def discard_inserted_comments(comments, last_note) + last_note_attrs = nil + + cut_off_index = comments.find_index do |raw| + comment = CommentFormatter.new(project, raw) + comment_attrs = comment.attributes + last_note_attrs ||= last_note.slice(*comment_attrs.keys) + + comment_attrs.with_indifferent_access == last_note_attrs + end + + # No matching resource in the collection, which means we got halted right on the end of the last page, so all good + return unless cut_off_index + + # Otherwise, remove the resources we've already inserted + comments.shift(cut_off_index + 1) + end + def import_wiki unless project.wiki.repository_exists? wiki = WikiFormatter.new(project) @@ -192,7 +222,7 @@ module Gitlab end def import_releases - client.releases(repo, per_page: 100) do |releases| + fetch_resources(:releases, repo, per_page: 100) do |releases| releases.each do |raw| begin gh_release = ReleaseFormatter.new(project, raw) @@ -203,6 +233,47 @@ module Gitlab end end end + + def fetch_resources(resource_type, *opts) + return if imported?(resource_type) + + opts.last.merge!(page: current_page(resource_type)) + + client.public_send(resource_type, *opts) do |resources| + yield resources + increment_page(resource_type) + end + + imported!(resource_type) + end + + def imported?(resource_type) + Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported") + end + + def imported!(resource_type) + Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day) + end + + def increment_page(resource_type) + key = "#{cache_key_prefix}:#{resource_type}:current-page" + + # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is + # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around + page = Rails.cache.read(key) + page += 1 + Rails.cache.write(key, page) + + page + end + + def current_page(resource_type) + Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 } + end + + def cache_key_prefix + @cache_key_prefix ||= "github-import:#{project.id}" + end end end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 77621de9f4c..8c32ac59fc5 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -20,8 +20,12 @@ module Gitlab raw_data.comments > 0 end - def klass - Issue + def project_association + :issues + end + + def find_condition + { iid: number } end def number diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 942dfb3312b..002494739e9 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -9,8 +9,8 @@ module Gitlab } end - def klass - Label + def project_association + :labels end def create! diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index b2fa524cf5b..401dd962521 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -14,8 +14,12 @@ module Gitlab } end - def klass - Milestone + def project_association + :milestones + end + + def find_condition + { iid: raw_data.number } end private diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 1408683100f..b9a227fb11a 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,8 +24,12 @@ module Gitlab } end - def klass - MergeRequest + def project_association + :merge_requests + end + + def find_condition + { iid: number } end def number diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb index 73d643b00ad..1ad702a6058 100644 --- a/lib/gitlab/github_import/release_formatter.rb +++ b/lib/gitlab/github_import/release_formatter.rb @@ -11,8 +11,12 @@ module Gitlab } end - def klass - Release + def project_association + :releases + end + + def find_condition + { tag: raw_data.tag_name } end def valid? diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 84f280ceaae..7478f86bd28 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -2,6 +2,10 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do describe '#execute' do + before do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + end + context 'when an error occurs' do let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } let(:octocat) { double(id: 123456, login: 'octocat') } @@ -152,10 +156,9 @@ describe Gitlab::GithubImport::Importer, lib: true do message: 'The remote data could not be fully imported.', errors: [ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, - { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" }, + { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } ] diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index ed1384798ab..ab6e8f537ba 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -110,7 +110,7 @@ describe Projects::ImportService, services: true do end it 'expires existence cache after error' do - allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true) + allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true) expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original -- GitLab From 4ec7da18aec4fd99f8fee371fe880fedb3fdb863 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 26 Oct 2016 12:12:10 +0000 Subject: [PATCH 099/109] Merge branch '21248-wrong-urlencoding-when-switching-branch-in-graphs-contributers' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix refs dropdown selection with special characters Remove unneeded encode from the project-refs-dropdown renderRow method. Closes #21248 See merge request !7061 Signed-off-by: Rémy Coutable --- app/assets/javascripts/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index a6c015299a0..af0530b7159 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -82,7 +82,7 @@ if (ref.header != null) { return $('
  • ').addClass('dropdown-header').text(ref.header); } else { - link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref); return $('
  • ').append(link); } }, -- GitLab From 76753758d0102d2a2acd9f4c6e1922eb45296ddc Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 26 Oct 2016 16:08:57 +0000 Subject: [PATCH 100/109] Merge branch '23661-lacking-padding-on-syntax-highlight-blocks-in-diff-comments' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve "Lacking padding on syntax highlight blocks in diff comments" Enforces horizontal padding on highlight block. There was no horizontal padding on diff comment highlight blocks. Closes #23661 See merge request !7062 Signed-off-by: Rémy Coutable --- app/views/projects/diffs/_parallel_view.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 28aad3f4725..78aa9fb7391 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data } +%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - last_line = 0 - diff_file.parallel_diff_lines.each do |line| -- GitLab From 9addb14773fb7fb8b31cf2e61c8a54ee347c979a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 28 Oct 2016 15:01:59 +0000 Subject: [PATCH 101/109] Merge branch 'adam-fix-labels-find-or-create' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass user instance to Labels::FindOrCreateService or skip_authorization: true It fixes a bug described in #23694 when `project.owner` was passed to `Labels::FindOrCreateService`. `Labels::FindOrCreateService` expected a user instance and `project.owner` may return a group as well. This MR makes sure that we either pass a user instance or `skip_authorization: true`. Fixes #23694 See merge request !7093 Signed-off-by: Rémy Coutable --- app/finders/labels_finder.rb | 15 +++--- app/models/project.rb | 2 +- app/services/labels/find_or_create_service.rb | 11 ++-- lib/banzai/filter/label_reference_filter.rb | 2 +- lib/gitlab/fogbugz_import/importer.rb | 4 +- lib/gitlab/github_import/label_formatter.rb | 4 +- lib/gitlab/google_code_import/importer.rb | 4 +- lib/gitlab/issues_labels.rb | 2 +- .../projects/labels_controller_spec.rb | 21 ++++++-- .../labels/find_or_create_service_spec.rb | 51 +++++++++++-------- 10 files changed, 70 insertions(+), 46 deletions(-) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 44484d64567..865f093f04a 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -4,9 +4,8 @@ class LabelsFinder < UnionFinder @params = params end - def execute(authorized_only: true) - @authorized_only = authorized_only - + def execute(skip_authorization: false) + @skip_authorization = skip_authorization items = find_union(label_ids, Label) items = with_title(items) sort(items) @@ -14,7 +13,7 @@ class LabelsFinder < UnionFinder private - attr_reader :current_user, :params, :authorized_only + attr_reader :current_user, :params, :skip_authorization def label_ids label_ids = [] @@ -70,17 +69,17 @@ class LabelsFinder < UnionFinder end def find_project - if authorized_only - available_projects.find_by(id: project_id) - else + if skip_authorization Project.find_by(id: project_id) + else + available_projects.find_by(id: project_id) end end def projects return @projects if defined?(@projects) - @projects = authorized_only ? available_projects : Project.all + @projects = skip_authorization ? Project.all : available_projects @projects = @projects.in_namespace(group_id) if group_id @projects = @projects.where(id: projects_ids) if projects_ids @projects = @projects.reorder(nil) diff --git a/app/models/project.rb b/app/models/project.rb index 6685baab699..a5c1d5c9e31 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -738,7 +738,7 @@ class Project < ActiveRecord::Base def create_labels Label.templates.each do |label| params = label.attributes.except('id', 'template', 'created_at', 'updated_at') - Labels::FindOrCreateService.new(owner, self, params).execute + Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true) end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 74291312c4e..d622f9edd33 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -2,21 +2,24 @@ module Labels class FindOrCreateService def initialize(current_user, project, params = {}) @current_user = current_user - @group = project.group @project = project @params = params.dup end - def execute + def execute(skip_authorization: false) + @skip_authorization = skip_authorization find_or_create_label end private - attr_reader :current_user, :group, :project, :params + attr_reader :current_user, :project, :params, :skip_authorization def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute + @available_labels ||= LabelsFinder.new( + current_user, + project_id: project.id + ).execute(skip_authorization: skip_authorization) end def find_or_create_label diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index c24831e68ee..9f9a96cdc65 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai end def find_labels(project) - LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false) + LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true) end # Parameters to pass to `Label.find_by` based on the given arguments diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 65ee85ca5a9..222bcdcbf9c 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -75,7 +75,7 @@ module Gitlab def create_label(name) params = { title: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def user_info(person_id) @@ -133,7 +133,7 @@ module Gitlab updated_at: DateTime.parse(bug['dtLastUpdated']) ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 002494739e9..211ccdc51bb 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -15,8 +15,8 @@ module Gitlab def create! params = attributes.except(:project) - service = ::Labels::FindOrCreateService.new(project.owner, project, params) - label = service.execute + service = ::Labels::FindOrCreateService.new(nil, project, params) + label = service.execute(skip_authorization: true) raise ActiveRecord::RecordInvalid.new(label) unless label.persisted? diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 6a68e786b4f..1f4edc36928 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -101,7 +101,7 @@ module Gitlab state: raw_issue['state'] == 'closed' ? 'closed' : 'opened' ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) @@ -235,7 +235,7 @@ module Gitlab def create_label(name) params = { name: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def format_content(raw_content) diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index dbc759367eb..b8ca7f2f55f 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -19,7 +19,7 @@ module Gitlab ] labels.each do |params| - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 41df63d445a..8faecec0063 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::LabelsController do let(:group) { create(:group) } - let(:project) { create(:project, namespace: group) } + let(:project) { create(:empty_project, namespace: group) } let(:user) { create(:user) } before do @@ -73,16 +73,27 @@ describe Projects::LabelsController do describe 'POST #generate' do let(:admin) { create(:admin) } - let(:project) { create(:empty_project) } before do sign_in(admin) end - it 'creates labels' do - post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + context 'personal project' do + let(:personal_project) { create(:empty_project) } - expect(response).to have_http_status(302) + it 'creates labels' do + post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param + + expect(response).to have_http_status(302) + end + end + + context 'project belonging to a group' do + it 'creates labels' do + post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to have_http_status(302) + end end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index cbfc63de811..7a9b34f9f96 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe Labels::FindOrCreateService, services: true do describe '#execute' do - let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, namespace: group) } @@ -14,37 +13,49 @@ describe Labels::FindOrCreateService, services: true do } end - subject(:service) { described_class.new(user, project, params) } - - before do - project.team << [user, :developer] - end + context 'when acting on behalf of a specific user' do + let(:user) { create(:user) } + subject(:service) { described_class.new(user, project, params) } + before do + project.team << [user, :developer] + end - context 'when label does not exist at group level' do - it 'creates a new label at project level' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project level' do + expect { service.execute }.to change(project.labels, :count).by(1) + end end - end - context 'when label exists at group level' do - it 'returns the group label' do - group_label = create(:group_label, group: group, title: 'Security') + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') - expect(service.execute).to eq group_label + expect(service.execute).to eq group_label + end end - end - context 'when label does not exist at group level' do - it 'creates a new label at project leve' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project leve' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end + + context 'when label exists at project level' do + it 'returns the project label' do + project_label = create(:label, project: project, title: 'Security') + + expect(service.execute).to eq project_label + end end end - context 'when label exists at project level' do + context 'when authorization is not required' do + subject(:service) { described_class.new(nil, project, params) } + it 'returns the project label' do project_label = create(:label, project: project, title: 'Security') - expect(service.execute).to eq project_label + expect(service.execute(skip_authorization: true)).to eq project_label end end end -- GitLab From cd7212ef69419202d90e11ca8032c2daf603910a Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 27 Oct 2016 15:29:28 +0000 Subject: [PATCH 102/109] Merge branch '23866-builds-dropdown' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Increase z index on fixed mr tabs Before: After: Closes #23866 See merge request !7124 Signed-off-by: Rémy Coutable --- app/assets/stylesheets/pages/merge_requests.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 70afa568554..33f09722274 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -439,12 +439,12 @@ } .merge-request-tabs-holder { - background-color: #fff; + background-color: $white-light; &.affix { top: 100px; left: 0; - z-index: 9; + z-index: 10; transition: right .15s; } -- GitLab From e35ecccb3012501ee30b72196aba2d096b4d3b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 28 Oct 2016 16:33:23 +0000 Subject: [PATCH 103/109] Merge branch '23872-members-of-group-that-has-project-access-getting-404-on-accessing-a-project-issue' into 'master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix project member access for group links Closes #23872. See merge request !7144 Signed-off-by: Rémy Coutable --- app/models/project_team.rb | 10 +- spec/policies/issues_policy_spec.rb | 193 ++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 spec/policies/issues_policy_spec.rb diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 79d041d2775..a6e911df9bd 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -125,14 +125,8 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user, min_member_access = nil) - member = !!find_member(user.id) - - if min_member_access - member && max_member_access(user.id) >= min_member_access - else - member - end + def member?(user, min_member_access = Gitlab::Access::GUEST) + max_member_access(user.id) >= min_member_access end def human_max_access(user_id) diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb new file mode 100644 index 00000000000..2b7b6cad654 --- /dev/null +++ b/spec/policies/issues_policy_spec.rb @@ -0,0 +1,193 @@ +require 'spec_helper' + +describe IssuePolicy, models: true do + let(:guest) { create(:user) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:reporter) { create(:user) } + let(:group) { create(:group, :public) } + let(:reporter_from_group_link) { create(:user) } + + def permissions(user, issue) + IssuePolicy.abilities(user, issue).to_set + end + + context 'a private project' do + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [author, :guest] + project.team << [assignee, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'does not allow non-members to read issues' do + expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow non-members to read confidential issues' do + expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end + + context 'a public project' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporter from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end +end -- GitLab From d1d73f28afd089e10136e8ad98f640345471259d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 31 Oct 2016 13:17:00 +0100 Subject: [PATCH 104/109] Fix CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG.md | 375 ++++++++++++++++++++++++--------------------------- 1 file changed, 174 insertions(+), 201 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19dd803b38c..f47814ab3bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,212 +1,185 @@ Please view this file on the master branch, on stable branches it's out of date. -## 8.14.0 (2016-11-22) - - Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 - - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - - Trim leading and trailing whitespace on project_path (Linus Thiel) - - Prevent award emoji via notes for issues/MRs authored by user (barthc) - - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - - Fix extra space on Build sidebar on Firefox !7060 - - Fix mobile layout issues in admin user overview page !7087 - - Fix HipChat notifications rendering (airatshigapov, eisnerd) - - Refactor Jira service to use jira-ruby gem - - Add hover to trash icon in notes !7008 (blackst0ne) - - Only show one error message for an invalid email !5905 (lycoperdon) - - Fix sidekiq stats in admin area (blackst0ne) - - API: Fix booleans not recognized as such when using the `to_boolean` helper - - Removed delete branch tooltip !6954 - - Stop unauthorized users dragging on milestone page (blackst0ne) - - Restore issue boards welcome message when a project is created !6899 - - Escape ref and path for relative links !6050 (winniehell) - - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) - - Fix filtering of milestones with quotes in title (airatshigapov) - - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) - - Update mail_room and enable sentinel support to Reply By Email (!7101) - - Simpler arguments passed to named_route on toggle_award_url helper method - - Fix typo in framework css class. !7086 (Daniel Voogsgerd) - - New issue board list dropdown stays open after adding a new list - - Fix: Backup restore doesn't clear cache - - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) - - Replace jquery.cookie plugin with js.cookie !7085 - - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method - - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - - Show full status link on MR & commit pipelines - - Fix documents and comments on Build API `scope` - - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - - Shortened merge request modal to let clipboard button not overlap - -## 8.13.2 - - Fix builds dropdown overlapping bug !7124 - - Fix applying labels for GitHub-imported MRs !7139 - - Fix importing MR comments from GitHub !7139 - - Modify GitHub importer to be retryable !7003 - - Fix and improve `Sortable.highest_label_priority` - - Fixed sticky merge request tabs when sidebar is pinned - - Fix and improve `Sortable.highest_label_priority` +## 8.13.2 (2016-10-31) + +- Fix encoding issues on pipeline commits. !6832 +- Use Hash rocket syntax to fix cycle analytics under Ruby 2.1. !6977 +- Modify GitHub importer to be retryable. !7003 +- Fix refs dropdown selection with special characters. !7061 +- Fix horizontal padding for highlight blocks. !7062 +- Pass user instance to `Labels::FindOrCreateService` or `skip_authorization: true`. !7093 +- Fix builds dropdown overlapping bug. !7124 +- Fix applying labels for GitHub-imported MRs. !7139 +- Fix importing MR comments from GitHub. !7139 +- Fix project member access for group links. !7144 +- API: Fix booleans not recognized as such when using the `to_boolean` helper. !7149 +- Fix and improve `Sortable.highest_label_priority`. !7165 +- Fixed sticky merge request tabs when sidebar is pinned. !7167 +- Only remove right connector of first build of last stage. !7179 ## 8.13.1 (2016-10-25) - - Fix branch protection API. !6215 - - Fix hidden pipeline graph on commit and MR page. !6895 - - Fix Cycle analytics not showing correct data when filtering by date. !6906 - - Ensure custom provider tab labels don't break layout. !6993 - - Fix issue boards user link when in subdirectory. !7018 - - Refactor and add new environment functionality to CI yaml reference. !7026 - - Fix typo in project settings that prevents users from enabling container registry. !7037 - - Fix events order in `users/:id/events` endpoint. !7039 - - Remove extra line for empty issue description. !7045 - - Don't append issue/MR templates to any existing text. !7050 - - Fix error in generating labels. !7055 - - Stop clearing the database cache on `rake cache:clear`. !7056 - - Only show register tab if signup enabled. !7058 - - Expire and build repository cache after project import. !7064 - - Fix bug where labels would be assigned to issues that were moved. !7065 - - Fix reply-by-email not working due to queue name mismatch. !7068 - - Fix 404 for group pages when GitLab setup uses relative url. !7071 - - Fix `User#to_reference`. !7088 - - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 - - Fix unauthorized users dragging on issue boards. !7096 - - Only schedule `ProjectCacheWorker` jobs when needed. !7099 +- Fix branch protection API. !6215 +- Fix hidden pipeline graph on commit and MR page. !6895 +- Fix Cycle analytics not showing correct data when filtering by date. !6906 +- Ensure custom provider tab labels don't break layout. !6993 +- Fix issue boards user link when in subdirectory. !7018 +- Refactor and add new environment functionality to CI yaml reference. !7026 +- Fix typo in project settings that prevents users from enabling container registry. !7037 +- Fix events order in `users/:id/events` endpoint. !7039 +- Remove extra line for empty issue description. !7045 +- Don't append issue/MR templates to any existing text. !7050 +- Fix error in generating labels. !7055 +- Stop clearing the database cache on `rake cache:clear`. !7056 +- Only show register tab if signup enabled. !7058 +- Expire and build repository cache after project import. !7064 +- Fix bug where labels would be assigned to issues that were moved. !7065 +- Fix reply-by-email not working due to queue name mismatch. !7068 +- Fix 404 for group pages when GitLab setup uses relative url. !7071 +- Fix `User#to_reference`. !7088 +- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 +- Fix unauthorized users dragging on issue boards. !7096 +- Only schedule `ProjectCacheWorker` jobs when needed. !7099 ## 8.13.0 (2016-10-22) - - Fix save button on project pipeline settings page. (!6955) - - All Sidekiq workers now use their own queue - - Avoid race condition when asynchronously removing expired artifacts. (!6881) - - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - - Respond with 404 Not Found for non-existent tags (Linus Thiel) - - Truncate long labels with ellipsis in labels page - - Improve tabbing usability for sign in page (ClemMakesApps) - - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint - - Adding members no longer silently fails when there is extra whitespace - - Update runner version only when updating contacted_at - - Add link from system note to compare with previous version - - Use gitlab-shell v3.6.6 - - Ignore references to internal issues when using external issues tracker - - Ability to resolve merge request conflicts with editor !6374 - - Add `/projects/visible` API endpoint (Ben Boeckel) - - Fix centering of custom header logos (Ashley Dumaine) - - Keep around commits only pipeline creation as pipeline data doesn't change over time - - Update duration at the end of pipeline - - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - - Add group level labels. (!6425) - - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - - Cancelled pipelines could be retried. !6927 - - Updating verbiage on git basics to be more intuitive - - Fix project_feature record not generated on project creation - - Clarify documentation for Runners API (Gennady Trafimenkov) - - The instrumentation for Banzai::Renderer has been restored - - Change user & group landing page routing from /u/:username to /:username - - Added documentation for .gitattributes files - - Move Pipeline Metrics to separate worker - - AbstractReferenceFilter caches project_refs on RequestStore when active - - Replaced the check sign to arrow in the show build view. !6501 - - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - - ProjectCacheWorker updates caches at most once per 15 minutes per project - - Fix Error 500 when viewing old merge requests with bad diff data - - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - - Fix viewing merged MRs when the source project has been removed !6991 - - Speed-up group milestones show page - - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService - - Fix discussion thread from emails for merge requests. !7010 - - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - - Add tag shortcut from the Commit page. !6543 - - Keep refs for each deployment - - Allow browsing branches that end with '.atom' - - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) - - Add more tests for calendar contribution (ClemMakesApps) - - Update Gitlab Shell to fix some problems with moving projects between storages - - Cache rendered markdown in the database, rather than Redis - - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - - Simplify Mentionable concern instance methods - - API: Ability to retrieve version information (Robert Schilling) - - Fix permission for setting an issue's due date - - API: Multi-file commit !6096 (mahcsig) - - Unicode emoji are now converted to images - - Revert "Label list shows all issues (opened or closed) with that label" - - Expose expires_at field when sharing project on API - - Fix VueJS template tags being rendered in code comments - - Added copy file path button to merge request diff files - - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - - Add Issue Board API support (andrebsguedes) - - Allow the Koding integration to be configured through the API - - Add new issue button to each list on Issues Board - - Execute specific named route method from toggle_award_url helper method - - Added soft wrap button to repository file/blob editor - - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - - Show the time ago a merge request was deployed to an environment - - Add RTL support to markdown renderer (Ebrahim Byagowi) - - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - - Fix todos page mobile viewport layout (ClemMakesApps) - - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - - Remove redundant mixins (ClemMakesApps) - - Added 'Download' button to the Snippets page (Justin DiPierro) - - Add visibility level to project repository - - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - - Fix that manual jobs would no longer block jobs in the next stage. !6604 - - Add configurable email subject suffix (Fu Xu) - - Use defined colour for a language when available !6748 (nilsding) - - Added tooltip to fork count on project show page. (Justin DiPierro) - - Use a ConnectionPool for Rails.cache on Sidekiq servers - - Replace `alias_method_chain` with `Module#prepend` - - Enable GitLab Import/Export for non-admin users. - - Preserve label filters when sorting !6136 (Joseph Frazier) - - MergeRequest#new form load diff asynchronously - - Only update issuable labels if they have been changed - - Take filters in account in issuable counters. !6496 - - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - - Append issue template to existing description !6149 (Joseph Frazier) - - Trending projects now only show public projects and the list of projects is cached for a day - - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) - - Revoke button in Applications Settings underlines on hover. - - Use higher size on Gitlab::Redis connection pool on Sidekiq servers - - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - - Revert avoid touching file system on Build#artifacts? - - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created - - Add disabled delete button to protected branches (ClemMakesApps) - - Add broadcast messages and alerts below sub-nav - - Better empty state for Groups view - - API: New /users/:id/events endpoint - - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - - Replace bootstrap caret with fontawesome caret (ClemMakesApps) - - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - - Add organization field to user profile - - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being. - - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - - Fix deploy status responsiveness error !6633 - - Make searching for commits case insensitive - - Fix resolved discussion display in side-by-side diff view !6575 - - Optimize GitHub importing for speed and memory - - API: expose pipeline data in builds API (!6502, Guilherme Salazar) - - Notify the Merger about merge after successful build (Dimitris Karakasilis) - - Reduce queries needed to find users using their SSH keys when pushing commits - - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - - Fix broken repository 500 errors in project list - - Fix the diff in the merge request view when converting a symlink to a regular file - - Fix Pipeline list commit column width should be adjusted - - Close todos when accepting merge requests via the API !6486 (tonygambone) - - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) - - Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - - Retouch environments list and deployments list - - Add multiple command support for all label related slash commands !6780 (barthc) - - Add Container Registry on/off status to Admin Area !6638 (the-undefined) - - Add Nofollow for uppercased scheme in external urls !6820 (the-undefined) - - Allow empty merge requests !6384 (Artem Sidorenko) - - Grouped pipeline dropdown is a scrollable container - - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) - - Fixes padding in all clipboard icons that have .btn class - - Fix a typo in doc/api/labels.md - - Fix double-escaping in activities tab (Alexandre Maia) - - API: all unknown routing will be handled with 404 Not Found - - Add docs for request profiling - - Delete dynamic environments - - Fix buggy iOS tooltip layering behavior. - - Make guests unable to view MRs on private projects - - Fix broken Project API docs (Takuya Noguchi) - - Migrate invalid project members (owner -> master) +- Fix save button on project pipeline settings page. (!6955) +- All Sidekiq workers now use their own queue +- Avoid race condition when asynchronously removing expired artifacts. (!6881) +- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) +- Respond with 404 Not Found for non-existent tags (Linus Thiel) +- Truncate long labels with ellipsis in labels page +- Improve tabbing usability for sign in page (ClemMakesApps) +- Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint +- Adding members no longer silently fails when there is extra whitespace +- Update runner version only when updating contacted_at +- Add link from system note to compare with previous version +- Use gitlab-shell v3.6.6 +- Ignore references to internal issues when using external issues tracker +- Ability to resolve merge request conflicts with editor !6374 +- Add `/projects/visible` API endpoint (Ben Boeckel) +- Fix centering of custom header logos (Ashley Dumaine) +- Keep around commits only pipeline creation as pipeline data doesn't change over time +- Update duration at the end of pipeline +- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup +- Add group level labels. (!6425) +- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) +- Cancelled pipelines could be retried. !6927 +- Updating verbiage on git basics to be more intuitive +- Fix project_feature record not generated on project creation +- Clarify documentation for Runners API (Gennady Trafimenkov) +- The instrumentation for Banzai::Renderer has been restored +- Change user & group landing page routing from /u/:username to /:username +- Added documentation for .gitattributes files +- Move Pipeline Metrics to separate worker +- AbstractReferenceFilter caches project_refs on RequestStore when active +- Replaced the check sign to arrow in the show build view. !6501 +- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) +- ProjectCacheWorker updates caches at most once per 15 minutes per project +- Fix Error 500 when viewing old merge requests with bad diff data +- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) +- Fix viewing merged MRs when the source project has been removed !6991 +- Speed-up group milestones show page +- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) +- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService +- Fix discussion thread from emails for merge requests. !7010 +- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) +- Add tag shortcut from the Commit page. !6543 +- Keep refs for each deployment +- Allow browsing branches that end with '.atom' +- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) +- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) +- Add more tests for calendar contribution (ClemMakesApps) +- Update Gitlab Shell to fix some problems with moving projects between storages +- Cache rendered markdown in the database, rather than Redis +- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references +- Simplify Mentionable concern instance methods +- API: Ability to retrieve version information (Robert Schilling) +- Fix permission for setting an issue's due date +- API: Multi-file commit !6096 (mahcsig) +- Unicode emoji are now converted to images +- Revert "Label list shows all issues (opened or closed) with that label" +- Expose expires_at field when sharing project on API +- Fix VueJS template tags being rendered in code comments +- Added copy file path button to merge request diff files +- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) +- Add Issue Board API support (andrebsguedes) +- Allow the Koding integration to be configured through the API +- Add new issue button to each list on Issues Board +- Execute specific named route method from toggle_award_url helper method +- Added soft wrap button to repository file/blob editor +- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) +- Show the time ago a merge request was deployed to an environment +- Add RTL support to markdown renderer (Ebrahim Byagowi) +- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) +- Fix todos page mobile viewport layout (ClemMakesApps) +- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) +- Remove redundant mixins (ClemMakesApps) +- Added 'Download' button to the Snippets page (Justin DiPierro) +- Add visibility level to project repository +- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) +- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) +- Fix that manual jobs would no longer block jobs in the next stage. !6604 +- Add configurable email subject suffix (Fu Xu) +- Use defined colour for a language when available !6748 (nilsding) +- Added tooltip to fork count on project show page. (Justin DiPierro) +- Use a ConnectionPool for Rails.cache on Sidekiq servers +- Replace `alias_method_chain` with `Module#prepend` +- Enable GitLab Import/Export for non-admin users. +- Preserve label filters when sorting !6136 (Joseph Frazier) +- MergeRequest#new form load diff asynchronously +- Only update issuable labels if they have been changed +- Take filters in account in issuable counters. !6496 +- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) +- Append issue template to existing description !6149 (Joseph Frazier) +- Trending projects now only show public projects and the list of projects is cached for a day +- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) +- Revoke button in Applications Settings underlines on hover. +- Use higher size on Gitlab::Redis connection pool on Sidekiq servers +- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) +- Revert avoid touching file system on Build#artifacts? +- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created +- Add disabled delete button to protected branches (ClemMakesApps) +- Add broadcast messages and alerts below sub-nav +- Better empty state for Groups view +- API: New /users/:id/events endpoint +- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) +- Replace bootstrap caret with fontawesome caret (ClemMakesApps) +- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 +- Add organization field to user profile +- Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being. +- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) +- Fix deploy status responsiveness error !6633 +- Make searching for commits case insensitive +- Fix resolved discussion display in side-by-side diff view !6575 +- Optimize GitHub importing for speed and memory +- API: expose pipeline data in builds API (!6502, Guilherme Salazar) +- Notify the Merger about merge after successful build (Dimitris Karakasilis) +- Reduce queries needed to find users using their SSH keys when pushing commits +- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) +- Fix broken repository 500 errors in project list +- Fix the diff in the merge request view when converting a symlink to a regular file +- Fix Pipeline list commit column width should be adjusted +- Close todos when accepting merge requests via the API !6486 (tonygambone) +- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) +- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) +- Retouch environments list and deployments list +- Add multiple command support for all label related slash commands !6780 (barthc) +- Add Container Registry on/off status to Admin Area !6638 (the-undefined) +- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined) +- Allow empty merge requests !6384 (Artem Sidorenko) +- Grouped pipeline dropdown is a scrollable container +- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) +- Fixes padding in all clipboard icons that have .btn class +- Fix a typo in doc/api/labels.md +- Fix double-escaping in activities tab (Alexandre Maia) +- API: all unknown routing will be handled with 404 Not Found +- Add docs for request profiling +- Delete dynamic environments +- Fix buggy iOS tooltip layering behavior. +- Make guests unable to view MRs on private projects +- Fix broken Project API docs (Takuya Noguchi) +- Migrate invalid project members (owner -> master) ## 8.12.7 -- GitLab From 89a5afcfa8ac11c34f84c23e9ff9ad76d7261985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 31 Oct 2016 15:41:44 +0100 Subject: [PATCH 105/109] Update VERSION to 8.13.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index f61979f1181..e1451b2b872 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.1 +8.13.2 -- GitLab From 717d7b97725147d2e101a28f0d499f9d68bda56e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 26 Oct 2016 13:04:31 +0100 Subject: [PATCH 106/109] Removes any symlinks before importing a project export file. Also added relevant spec. --- lib/gitlab/import_export/file_importer.rb | 8 ++++ .../import_export/project_tree_restorer.rb | 10 ++++- lib/gitlab/import_export/version_checker.rb | 9 +++- .../import_export/file_importer_spec.rb | 42 +++++++++++++++++++ .../project_tree_restorer_spec.rb | 14 +++++++ .../import_export/version_checker_spec.rb | 16 ++++++- spec/support/import_export/common_util.rb | 10 +++++ 7 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 spec/lib/gitlab/import_export/file_importer_spec.rb create mode 100644 spec/support/import_export/common_util.rb diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 113895ba22c..ffd17118c91 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -43,6 +43,14 @@ module Gitlab raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result + remove_symlinks! + end + + def remove_symlinks! + Dir["#{@shared.export_path}/**/*"].each do |path| + FileUtils.rm(path) if File.lstat(path).symlink? + end + true end end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 7cdba880a93..c551321c18d 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -9,8 +9,14 @@ module Gitlab end def restore - json = IO.read(@path) - @tree_hash = ActiveSupport::JSON.decode(json) + begin + json = IO.read(@path) + @tree_hash = ActiveSupport::JSON.decode(json) + rescue => e + Rails.logger.error("Import/Export error: #{e.message}") + raise Gitlab::ImportExport::Error.new('Incorrect JSON format') + end + @project_members = @tree_hash.delete('project_members') ActiveRecord::Base.no_touching do diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb index fc08082fc86..bd3c3ee3b2f 100644 --- a/lib/gitlab/import_export/version_checker.rb +++ b/lib/gitlab/import_export/version_checker.rb @@ -24,12 +24,19 @@ module Gitlab end def verify_version!(version) - if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version) + if different_version?(version) raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}") else true end end + + def different_version?(version) + Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version) + rescue => e + Rails.logger.error("Import/Export error: #{e.message}") + raise Gitlab::ImportExport::Error.new('Incorrect VERSION format') + end end end end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb new file mode 100644 index 00000000000..a88ddd17aca --- /dev/null +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::FileImporter, lib: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" } + let(:valid_file) { "#{shared.export_path}/valid.json" } + let(:symlink_file) { "#{shared.export_path}/invalid.json" } + let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } + + before do + stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) + + setup_files + + described_class.import(archive_file: '', shared: shared) + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + + def setup_files + FileUtils.mkdir_p("#{shared.export_path}/subfolder/") + FileUtils.touch(valid_file) + FileUtils.ln_s(valid_file, symlink_file) + FileUtils.ln_s(valid_file, subfolder_symlink_file) + end +end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 069ea960321..3038ab53ad8 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do @@ -175,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) end end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original + + restored_project_json + + expect(shared.errors.first).not_to include('test') + end + end + end end end end diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index c680e712b59..2405ac5abfe 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' +include ImportExport::CommonUtil describe Gitlab::ImportExport::VersionChecker, services: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } + describe 'bundle a project Git repo' do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } let(:version) { Gitlab::ImportExport.version } before do @@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do end end end + + describe 'version file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'VERSION') + + described_class.check!(shared: shared) + + expect(shared.errors.first).not_to include('test') + end + end + end end diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb new file mode 100644 index 00000000000..2542a59bb00 --- /dev/null +++ b/spec/support/import_export/common_util.rb @@ -0,0 +1,10 @@ +module ImportExport + module CommonUtil + def setup_symlink(tmpdir, symlink_name) + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir) + + File.open("#{tmpdir}/test", 'w') { |file| file.write("test") } + FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}") + end + end +end -- GitLab From 1b0fa5b7694cbfeb0f53faa61a3a1bcca19c848e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 26 Oct 2016 18:44:11 +0100 Subject: [PATCH 107/109] Fixed Import/Export foreign key issue to do with project members --- lib/gitlab/import_export/attribute_cleaner.rb | 23 +++++++++++++++---- lib/gitlab/import_export/members_mapper.rb | 7 +++++- lib/gitlab/import_export/relation_factory.rb | 11 ++++----- .../import_export/attribute_cleaner_spec.rb | 7 ++++-- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb index f755a404693..34169319b26 100644 --- a/lib/gitlab/import_export/attribute_cleaner.rb +++ b/lib/gitlab/import_export/attribute_cleaner.rb @@ -3,10 +3,25 @@ module Gitlab class AttributeCleaner ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] - def self.clean!(relation_hash:) - relation_hash.reject! do |key, _value| - key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key) - end + def self.clean(*args) + new(*args).clean + end + + def initialize(relation_hash:, relation_class:) + @relation_hash = relation_hash + @relation_class = relation_class + end + + def clean + @relation_hash.reject do |key, _value| + prohibited_key?(key) || !@relation_class.attribute_method?(key) + end.except('id') + end + + private + + def prohibited_key?(key) + key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key) end end end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 36c4cf6efa0..b790733f4a7 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -55,7 +55,12 @@ module Gitlab end def member_hash(member) - member.except('id').merge(source_id: @project.id, importing: true) + parsed_hash(member).merge('source_id' => @project.id, 'importing' => true) + end + + def parsed_hash(member) + Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys, + relation_class: ProjectMember) end def find_project_user_query(member) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index dc630e76411..a0e80fccad9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -14,7 +14,7 @@ module Gitlab priorities: :label_priorities, label: :project_label }.freeze - USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze @@ -30,7 +30,7 @@ module Gitlab def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:) @relation_name = OVERRIDES[relation_sym] || relation_sym - @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id) + @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id) @members_mapper = members_mapper @user = user @imported_object_retries = 0 @@ -172,11 +172,8 @@ module Gitlab end def parsed_relation_hash - @parsed_relation_hash ||= begin - Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash) - - @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) } - end + @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash, + relation_class: relation_class) end def set_st_diffs diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index b8e7932eb4a..63bab0f0d0d 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::ImportExport::AttributeCleaner, lib: true do + let(:relation_class){ double('relation_class').as_null_object } let(:unsafe_hash) do { + 'id' => 101, 'service_id' => 99, 'moved_to_id' => 99, 'namespace_id' => 99, @@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do end it 'removes unwanted attributes from the hash' do - described_class.clean!(relation_hash: unsafe_hash) + # allow(relation_class).to receive(:attribute_method?).and_return(true) + parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class) - expect(unsafe_hash).to eq(post_safe_hash) + expect(parsed_hash).to eq(post_safe_hash) end end -- GitLab From 8d79ab3a090ee0a43659121b858692791be52cf8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 2 Nov 2016 13:18:06 +0000 Subject: [PATCH 108/109] Update VERSION to 8.13.3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e1451b2b872..3b3df401908 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.2 +8.13.3 -- GitLab From 8c8cd865fd040c3b5c027eceab281207f66fabcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 3 Nov 2016 11:02:13 +0100 Subject: [PATCH 109/109] Add 8.13.3 CHANGELOG entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f47814ab3bc..a3ac317b55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ Please view this file on the master branch, on stable branches it's out of date. +## 8.13.3 (2016-11-02) + +- Removes any symlinks before importing a project export file. CVE-2016-9086 +- Fixed Import/Export foreign key issue to do with project members. + ## 8.13.2 (2016-10-31) - Fix encoding issues on pipeline commits. !6832 -- GitLab