From 4b024c650c2070e0caf85feb822c023ac6af388f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 4 Nov 2019 15:16:37 +0100 Subject: [PATCH] implement squash to mergeCommand --- .../repository/api/MergeCommandBuilder.java | 12 +++++ .../scm/repository/api/ScmMergeStrategy.java | 5 ++ .../repository/spi/MergeCommandRequest.java | 16 +++++- .../scm/repository/spi/GitMergeCommand.java | 18 +++++-- .../repository/spi/GitMergeCommandTest.java | 51 ++++++++++++++++++ .../scm/repository/spi/scm-git-spi-test.zip | Bin 28443 -> 36171 bytes 6 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/ScmMergeStrategy.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java index 0a2267e888..df9f6bdfbe 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -92,6 +92,18 @@ public class MergeCommandBuilder { return this; } + /** + * Use this to set the strategy of the merge commit manually. + * + * This is optional and for {@link #executeMerge()} only. + * + * @return This builder instance. + */ + public MergeCommandBuilder setMergeStrategy(ScmMergeStrategy strategy) { + request.setScmMergeStrategy(strategy); + return this; + } + /** * Use this to set a template for the commit message. If no message is set, a default message will be used. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmMergeStrategy.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmMergeStrategy.java new file mode 100644 index 0000000000..7ff1b6bc51 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmMergeStrategy.java @@ -0,0 +1,5 @@ +package sonia.scm.repository.api; + +public enum ScmMergeStrategy { + SQUASH +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index 223cf8c49e..f7094a5e7f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -5,6 +5,7 @@ import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.Validateable; import sonia.scm.repository.Person; +import sonia.scm.repository.api.ScmMergeStrategy; import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; import java.io.Serializable; @@ -17,6 +18,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl private String targetBranch; private Person author; private String messageTemplate; + private ScmMergeStrategy scmMergeStrategy; public String getBranchToMerge() { return branchToMerge; @@ -50,6 +52,14 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl this.messageTemplate = messageTemplate; } + public ScmMergeStrategy getScmMergeStrategy() { + return scmMergeStrategy; + } + + public void setScmMergeStrategy(ScmMergeStrategy scmMergeStrategy) { + this.scmMergeStrategy = scmMergeStrategy; + } + public boolean isValid() { return !Strings.isNullOrEmpty(getBranchToMerge()) && !Strings.isNullOrEmpty(getTargetBranch()); @@ -74,12 +84,13 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl return Objects.equal(branchToMerge, other.branchToMerge) && Objects.equal(targetBranch, other.targetBranch) - && Objects.equal(author, other.author); + && Objects.equal(author, other.author) + && Objects.equal(scmMergeStrategy, other.scmMergeStrategy); } @Override public int hashCode() { - return Objects.hashCode(branchToMerge, targetBranch, author); + return Objects.hashCode(branchToMerge, targetBranch, author, scmMergeStrategy); } @Override @@ -88,6 +99,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl .add("branchToMerge", branchToMerge) .add("targetBranch", targetBranch) .add("author", author) + .add("mergeStrategy", scmMergeStrategy) .toString(); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index 5643c858b5..bafe6b8376 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -16,6 +16,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.api.ScmMergeStrategy; import java.io.IOException; import java.text.MessageFormat; @@ -61,6 +62,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand private final String toMerge; private final Person author; private final String messageTemplate; + private final ScmMergeStrategy scmMergeStrategy; private MergeWorker(Git clone, MergeCommandRequest request) { super(clone); @@ -68,6 +70,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand this.toMerge = request.getBranchToMerge(); this.author = request.getAuthor(); this.messageTemplate = request.getMessageTemplate(); + this.scmMergeStrategy = request.getScmMergeStrategy(); } @Override @@ -86,11 +89,18 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand MergeResult result; try { ObjectId sourceRevision = resolveRevision(toMerge); - result = getClone().merge() - .setFastForward(FastForwardMode.NO_FF) + org.eclipse.jgit.api.MergeCommand mergeCommand = getClone().merge(); + mergeCommand .setCommit(false) // we want to set the author manually - .include(toMerge, sourceRevision) - .call(); + .include(toMerge, sourceRevision); + + if (scmMergeStrategy == ScmMergeStrategy.SQUASH) { + mergeCommand.setSquash(true); + } else { + mergeCommand.setFastForward(FastForwardMode.NO_FF); + } + + result = mergeCommand.call(); } catch (GitAPIException e) { throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 5586a2f710..873dd23498 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -15,10 +15,12 @@ import org.junit.Test; import sonia.scm.NotFoundException; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.repository.api.ScmMergeStrategy; import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.user.User; import java.io.IOException; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -211,6 +213,55 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n"); } + @Test + public void shouldSquashCommitsIfSquashIsEnabled() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("this is a squash"); + request.setScmMergeStrategy(ScmMergeStrategy.SQUASH); + + MergeCommandResult mergeCommandResult = command.merge(request); + + Repository repository = createContext().open(); + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("this is a squash"); + } + + @Test + public void shouldSquashThreeCommitsIntoOne() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("squash three commits"); + request.setScmMergeStrategy(ScmMergeStrategy.SQUASH); + Repository gitRepository = createContext().open(); + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable commits = new Git(gitRepository).log().add(gitRepository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("squash three commits"); + + GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext(), repository); + List changes = modificationsCommand.getModifications("master").getAdded(); + assertThat(changes.size()).isEqualTo(3); + } + @Test(expected = NotFoundException.class) public void shouldHandleNotExistingSourceBranchInMerge() { GitMergeCommand command = createCommand(); diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 4310e637335c09568da9d3afa63ce57949cec5ab..8e43da1d828d94bc3f0bbe423d6488c88c52e5bf 100644 GIT binary patch delta 18069 zcmb_j34BfW@;^7hy=_R4h&|Sdh~@5!S`tY_Hc_$0mb*r535h1{K9<3_nw!K=5VNh0H>tO|a3eeW|532;NLEYwx z!Ae!e!{x(Xgsryp7c4EC%0B>d8hX)pcEXvDk9vbY9Tt;f; z$n^`ntpp{Vf9U$&MJ2=FSLKylfC#h#A5%ixR`PL1Xt5tCY3#Rm-+O)xgI|?bvXl>0 z&!&|FJn6Sgl{srp;bQ*E}v_fdvJ zz@E-*)D$gF$j;<*{=eTza$fJYXdaoF0z%xQ4q%uU1etzu(Xs9%-G6n2U~Wg3mty_E z-s`*W9MwVWQNm*{>_tI#iql1m@)^!=G_-VQyd%|Z(*1XRi0{2t!GW;RTEZqn0r*LF z=E`&tWwe4}nu5T~ty^Gf%-M5wgs_KLTkC_csDTS+%=-v5<2S`c9h4)Mgk1Opw@|M( zTxMikK9}mzk=f=%^Y7XFK?i*=-u&Z+lC5_QkC;yVX*>~qPVDdc2Bb>B1^$6ZcT3K*fEKXFfJXoE#%^Y73)~L zY2c-a+kxQR^n`1MaWFrAdjkqU*FfEcIir%h3Z zm;7kB&1MAz_RQ}%HD5V--ENodQQOv|%Z|R?;fw!%X`j-qUW{RnV}7reTa%8jdnM`C zn$b7LeW2U>_^(|hA8&}SP;4r1++*3YnWvskEIjhY8;J)NZ5;mAS26Qe-|hO#_}#sq zgtp)HkGCI99lEezN}c8-Uz=#|v^LuMcJrMf6AC#(5#vmfwYsK=3_PMyk<}tHFefV5t&ye#rRqmx zbYXXpL}?KbZB*a`rXI7KA_P1?_NJx{tX0F$N=hU<&ns7Td}QQvH8xv3mNWUa`hFz0Rw&>LPA2ny1*?hE zuob#S>{D$!!0}i=FJ=O0KrQZ^Ih{#!#-(?M1)HhLVX04W2z%yD9ft!2eBt2sOqmY% zOw;#^no0MJbk7d?Dg$a*Ppc@?gKlcO;pC0GQ4s()c?) zy!19VqW0C+HDMP$9rFw)Ya7@_p=o~HE($ufTDpr6OQp`AmS^c303R?oTiK%U22i2b zWzcN1@I3O2bScS-HoW~Gtz0C33UoKV2--)Fa-jY9*N$&yGxx;w?Ei~$`s({VuKl%QVVg&_ z{~FGPwlL=JvF$-CYIbKLzFpHk`4hKIPa_6sEe4mG&9t@0E(AgQioqvLLIqpU{ojyZ zs}9Auc7rC=CT4t9`Q_mEV9oQZm7S~7HS63WAio0B+HyfKuN|H&tcKA|{O1te#{nzmcTD|`ldHa8Jdi?j592c1m73`P2 zE`j|=A4DiGgCt7J`LC_9|2p1&Z&$Vc>zFSp*z_m^BpM!jgDyRT_56v0M{4CWke*Ouj6NgGR21$%aV3s}^f{p?{x&pp$8yhn# ze!4~=y?3v@(a6u z`*uy{PXC^zsf9rg&fM>QZRW3v;6tVAvb$Wiz8l_YUj(Q|_2Q~l)!Df04cwTqwW*wF zR1{k##C)bN7ey5Cm1w6Kbp;z1|0P6m$J%8Nz6ME@79n|Gj?pMO_=W&?5e5Z*;_l9O z-{nzBKJ`6^BS<=2PP@(JFxd1~t;=L`IrVC%#^!KpEl#6e?J~J6dcDnk#fbXj-1@!G zZw2m@X^IVHr^dy!sULOaUo#h7%vb)W_*$zKZ>Ov+(cFIjwRNV@t#>~-^GChyPebRY zJ)V3g_^ln;xeKEDU9A6FUaV&BorrnYJI9wAF05ST`eD0i{l4kPX7-6}wBgp+o%PpW z=`-Y;i(jjPhbO(%{!06}dn-q@%GtI!es1c;WqVH4xqCn0=kiuvf7)8|ZT2rCAKz1a zTRYhQu{A2E&ywObkDnCYpOLV0=9A<<1GXqXyF0KGmZ_{8Z#XJ%XBsOAza&>p`~hEm zl-GeIth*=5AZgR;twyy$uQyvvHlyBbvKVw$vs&k{IUF{Z#%grgjN0m>yp9?6+cz(QChz zU;KeNfCy`rTNb`%RPS{oFRww;U~y{oHnYy6)0iw)gTtiJI-Gi!#iG`j%np~qq|-Yr z7Vf@~==aVo&%5fyYo$xyy&Th_jwLiXv>vyA7jDZtjO7lEiw^ls5brAC1IaRK`J)gM z88-|3B%6gw9t26cj38;%TTKR?R%cUd&3cE~Wz;$>X1mn^J{mMGm(AqTsVy$fA)?>g zA;m$amuDHZYKO3l`PD4r0K?~-k3X6ok->)b`_+Ez(S);)ZqAza&fG_B1NQGOS?0$q z+cXHKT}su3MT;*T0rcoW&7$JOTNk_zCa`^kCDbb{AgZz0oNBev?$YTX!JS$Mtgp$c zGwV!NtIvh@$-B4XvJnKB_(CLTYO}{%oxo%=g`0#Pu4ea&<_jWe{%V@o3F1h%I zpn8usczFbpMzz&!(wUu56fHWP&SW;)4Q34#L-5FM(P+&Yy+LR6c!W0aKd-kM#Revw ztBTPt{zqLu>Xi$Y&+p{VeLJ}!J8MDngX7!(qiN(feaB77Up&MA)9ibXHkAFgS25$$ z@sYZW#ICiozjQ76@|6|-XPYFgZ?VnP*0SK-KZ`Tdr)NdYo0AlJ|Kdw4>dz^3u?r_( zpZ!%sM_$(*iF;F~&JB1_+HmBfUO$iSV7{-McT4&6{o=yg+k!Tfg{ymI)a|}@ZpL0? z`qq?Be>hU{-q(whMydA2J*d;WRoX+$n{{K`g>QjiU_itmdtYD@vMuQpB#cXpqT^6&!u7!=IHE}^k{lb6VLEgi}K#-@F z^&ID~furZ^?w|jjuV|O`dq9VOFMMJ1l-kqvs~UayVchZqyJo)ocVqd`9$`DvPclnx zWaYU&Dq3yol~khne#$2+);ohI4lCLj(6D9yrH9va|3TULrsKsh|5Zip+CKjASe>8V zyUA`W&Kq!KeQ?&&om2lDc5=w8)8>z_nD^Vzt>69n`jz-Ce;o-Q_n`KQ2i>R5&98H1 zc&CWZ*B!m~V_?bjjccwfN!pu|e&vh(Iq4NY_8GBi*UYl~MB9fOGVl2xzm_l+W50jv zyQ^@&Vw`FgdjnsZN$iCLWH*P`m05$UUTXjvAntmTLv7OPtZ)<=>{g4xska!+F15kn z(%J3s|J#lBYS-W@(^9NxpJe<$RkS{X-*GrXH{2Y2q!FCB2U}t*)D1Z!y|5 zcB@NkHyO=Zn^~{dxGXla(P%Znp$ApnV$z$di<_t94Zn8%g_b2y@uu|X+xaYbpj36$ zzTBen(dK>0kJDUnuZ;K9~k%~Y3%x6hICGP z<$T=ix898VYhb`U_Hv)er+Ye4LyqSron z9K6DKUQ&ggFo!uS8YH(U9<=$JS*QCgW(pT?I+q$Z^3vU5OF6e*D;`oPbqnk=x4d4= zk%hpym1h;G53j;ZLhwnX`%pnt+YplR?@8RWoeGpBVGtCp11D7u0^K$BKt&+fj!8{P zNfUTDBbH^+0OXY~$dp`^UWvI}-b-H>J zi?ZP(q%_&HC%LjRIU8i!w|wq6c)N67Z1=xm@ICylxC3cbNq|rAh<7-1?NhRH^0Klg zc~oJ;j9i1ZidXt7Ri?$tR=wfdC?V=vNJez;`!NhgJ~JzCd{?{GKGyw;qb0P34T>ue z$fF(4EX5aI6cANW8VZ)eTA)h8K=H9HA`??~4D6Vg5_$e~zm(*N^QTifcTMPW)g8#l ztlUkKTN0)V29nx3q!5TX4(Bwt^CI14$E_A-1wg*`C+qA0U&JpH$mF&I(+8O|sRI+@ z$9Qm`+AUI3yjq#s2WwhL(kfLFgt;VCbO2X>YlM3wY1)*BIJumOifPzZ>^USQYu zZzY3B;t}PkA(=*a$_tji$SP7>R3}d_+YZZD)yyOpu9BX zvI$R7!7)LH4`v%Cgg^X2B3roB{ORV54_X_@#0chbms6S=ZcpJUDrne(*WuZ$OT>Lo zZFk0^COqZQtHXFXu@e&}yI3s&0JjJz;C;gcBsW11QyzUijFF?ffr4jYd`UH$+vNQ_6mv)~FFxa=tCEM2_kv1QeaY$pU4pNu62Wj*%m}(STyhLit@@ z&aj$6!J%G#3l==+T@ZdBgpUvHeDHT9g9%T0^sJZQ$n`}H12u7i8cOFtaXg8Ir#$)? zQ6onc15KWKhf^KO@xrSt;b~vcpaWYl*no>nMm*?{2MHb7pqv!RKjFq)a7cz1hMGJ` zP!pwfNs-*?z$OggkdR5xGazXU4VZvLjtb_DgwnaBNHRLI4TgG<;3C7o;~9{+;J_D< z$Wg!miKiH2Oel>@iliihVSy5c%Y#87P~wF|p)DIO57G&Qr#yNryz2;13@1w9lG-il!p=$O>=G`RB!8;j-4DBp zNaRRg6bWjgY%M90sBUcX2o4G1fccET*>0j;IffV5_2ex^fYP$0cAKji78qZ+i(zPhzN{S@Kz_38x!VnPhmIR6%Z$_+DX+{x>97~G^1uG7vS4p9)HnLB~ za6$1p)M{8YyP8BKvXm@OM!{g6WYp8>=|obFpGCuo9USFYNqzW_i9x;=h7F$)ZPT1TX2Q393J@KE-$)re*ZO$+^%6kW=Tl8QgJutRKI?6K$)*q(-> z>MYQ|EIJ~`rJ{~t7EltD6v<4srJZxcD+#zA2}~ifaOKfKJw&r|yeYbHcuz^WPg1kP zdN9bN!Uzg|b5#o$(Ni=mN0_4i;_V|EN(Fg=gDW0eKk(B zE5~J`cCpA(K9bb#{y6rW+ph4A?A_^8?vZx{oIhv0*a!R9-BL7XQ>G^0s&w#v1vWQ%c zs^g8E@@1sRtx`?TN8OPir(7EEEmR);kl2&s(`$fJOVEacsDb99~o`P}}Z zJ&rd6JBE@>ypdDtiWGU7RMYd(Xr2MNGgU+`N00GFPWd2G(3Oz~bq7vDKy&1?eSpogr+SiQH(0@F6fBNdf{q z+9|W30YOkyuwsbB7xHh>39AA1d9Hczjf3(9==iRvpv6#z9i148tpXt@paM@wu=Ax9 z0jWb9heAlK?VF$mY<`6?Jaq||htBq;E@@$iEz%Tx5DO4dEQSalI@rhGr6Zir1by=S z02&)qLnB`CvtU^>)C z0)X%o6+8fC@^k=H!fgd%+DoVTwD=YktV?IAWCMVv+8iAAN^+x4qWRJS%Lw>KlfGyF zAlOUr11EEIG>@}>;VCW17|F;^{?QArD4-Xpf==b>fUvaSB&f)mBs0)Q9a$b2ur_of g(E(v;LF{Oz=F|KENww>s2f6TPGFf*%&^B;+PG@6z*jS4We+ubOf@+*tto3-HS_=boPEyT_Z;ppw6@EZ`}@ED zwg0`(rvrPf`z~2F&8`g!4kh@r_x9$xj4zHq84kb46+se6RefDu&Ag@4s%z%e-7=$L z-bfO(<)y0&M?bY~@#>y1VhR2%h!FNmo6p_iU6<69-QCk2ibkWr#0YXr`#^=~V#?3+ z;s1QFhJ#U;uie|y%#7VP{JjhHtv4K?8Mm*<7coWmhSkKuFjT@?iOFW z0tDRJL)+-Px6jGrxNT*{^{3L$?>GmTJyt?O0oUBpy)xU?=vwY!u)IUt79(uc!Vfp` zxJwo_*JZVI9T*2NT)_ZyPOGd|cwWA`5d8Cxw$tM+O@d##a+Z4<-Ca3tja}WIP8PS4 zOpF_BX+IeEa-VNyR}ObSxc=_&12nlICR)kTHCxBUhasB>hoc(_yBg!dAlvsroS#C* zk8D32G?@pA#-r!HbZAX5A@LD}jM0p*?r=ADd$PM4+d5j6h`E8jZNa|^ju$N~yS{6F zsD+SMVa1aKI%#C0$k?}hWV}TJ?7iXoAGbq9$6>_);Hl7f5kTJw-6!pza5;PT#SlVn z8^a=^4~I!1^_>d)B18g=vmQUO8w&R}#AO45^2VD)g1+#mgH{QU@XLiIKY=$p)DW(W zl|u+07h$OoWB&a6{>baF`7TH_Bv~zq%h%S{(mk=QtJySjzL*i#iQEcy%SYc+Pbye> z1=cXI($ens^l0t!%#+*teqK~;0f>1oC=jntYX#m3IZ7w2EjyEZ2%`EL)(y-&?Dh1x zT35UEnjU}g>OvY6e+E~Gg-}#3xPvwIv_Ao+5LP=Pvr6GY)_svYGw%U2w{qs2jaK5G$ z9#^+oC7xBNQm#^h7yPCMPCgDP%vJiO7HYc&Tm+O_xaAPf;=)mRH$p79av`4VRL3$1 ze?Jrsm*24NvDEgzQ9sw-j15aL484e0?%PZq?`FVkCxL~OSQBEC{s z`A`9n;zHun83Bn?GK&g$wElNPxKQPeM%PM(76&)pjLI3l(Kjwu=!GI>&+HG(df|7+ zXjd(`g$rGa&0?-$AD9)QBdd-Lks~WbihMPCJV0{0;b~ice#@G(U>+Az>$DbJ;&GM4h_#oc&e6;bJMD$Vk=A2GW%5*EG^V2dhZ! zYU9B|RNvOQ1LDED(#8%JZeDNfYW-k+3l zxInSTXHJi!qw;d;H}+&!Te*Gm)rgJlQ|%k5__t$gRWqa*7p8fqkI~`gZ{X7LukEGW z7Dn-CVsyM%ORqzT2`?JCfg*t!ftyW8dMsHPGwT}c`7T;EJq;p@ z4kera8Fz(cm&(Bvlniz@FgVxA>C^flFkBd1#VReh9%DIarsK5MLwGF#O8XImEy`C& zbKriGgIy29xxqT_OR<0}3K(fW5aROB=*faDETgMB#RKVRQYglop7~L)|5|F(uR9%0 zLz({{LYuCzF4NmI&~R#L7auKZobbTdSR^`HqFz+^7Lap+4~N6Ej%<1za)=8HaJh)( zLfvf0MPYHj$#BZ^D!DKYr)Qx^4^{1~P6qS1kmAQ~$9%}eClRbR{uKPkI+~py_rGzv zw!t}&GuPQ)bGq&mD{*}maz@V$PQHDwRC%8R!?@7+9jlrDswe)&X?y7lCbe{{J!)Rd z;zt(D5=YM7NkhBK!QY5hYoqQjKD@w9?MLibk9vjR3rwyOq8>Wg+Fu7aTo`5MS{5aL zuK9QD>B;pbRVr2|fU!!L$jASZ8hIy}$AuL4muZPKyFxxK+o+wyKyl@(tzjP555LI> zxd?=~ko4UiEiOocp?W#x@0rAv7cj0ZpFOZ4AI#%Iij5TkO^o!pE0WKu@a6Kcz&1Qr6a@9dCP0<0`I@hgChR+RC5>fr_n|%B;A4z|# zb`(OIa5KK&{n}cc3{Nj}&U2owor7=X@ec$Svt;?rJcmo6HJ^EBU;Scfr_%ENMUDF)k@ki0ci*7EuqTWu8a{94=PL8IDa5FK!+@UV9#V z7R)?*F01BD)*NS6|G5Td=FHQzXRlbvgSU>nuJ=ly3(>14M7aN z|F@{u;6e{APL>)2Oc9yreCL~0{25&W0si*m-E|oN#f9Hx^>b@xEY-ft&cJg8)E=E& zvEp^naA&GLfLeoJ==!I!@Df+Q)65R4pa!uIY&JX zR1L|~+3aa-Qgs__3=`q0B$wuNr`iPHaTxBl-8I0i!q+mPtw}KnP-q;2CPwnT^wXZy zM8OD_Ld@ljP>LhBMxhNXg^3L%iJ~Jo)UQ9a?q==?s#5f%CmN056<}f{&r7$iPOW5) zD0(4gc2VK$h3RThF&ZJWp0iVN^wR6GwYjK{TR93Kl>k zrAORRMLZ8ieN;5C#Y$AQ6v;s?RijsO=>M7=4%&?J6G^UBUIZ)`MFrP0xa)|PWl^2^ zT5oI+0g*IB08STyvOp>VM}%-%Ke<#eCHWvLrJ|{UFoL?N=!ArnB~uZZ8BS0@9TX{x zry_EA1VJfPM5c>@%POjfw2!4z8dF4|m_Ctk~*!=IDNGZ0z+6aoZ z!vP5w51wDGv_`pBkLze}ViG~|7E6m)4!Wpy6J9N{aH}Kt_a-OPYi=6?@tQ%PMv2#n ztl)~kf}05HxrjtPSE9sAM3!{jz$;ZX3R4O{A=pAk1US=w!2&8z zYO)m*M){YUu#oY+-=z^0gE4jx05NA}oZG*n!X(!_;R@!iGvQbFUg}9%_7$n0PbVk> z3uzxB^&9Yn;M7e19fVpIe?{tuOoCGI|4;p6rcTWS;6bQm`B$W_$Ra2P3)B@n;r_Q_ zxRmfKSvs{U0|PmK60o@eO4oSKh5eB>L5CFL2R#5#r!(}2ED3WH*dMJE2nxhPe=sHx zgQW>|(?p$#NyEUToPz)yc354OJA7WW27ReXb>E*TpMRh3zz}EF)*_wS__Si)f;cRA`%~o^0m7tW@Rq+k zwDKF3V$s{t#RPR@EI&swD*vH1twi^ZNyY$BXHObGyGFrSWZF)D=q^Ma6py8R;;@so zU=cUAl&)LN@~QHf7OrTL?jMtcfruAQa}WzBE5ahtopkn^LZNP2a~d^L4#VdBUSMWQ zFfj91oW_hMWFeTFz~(HY>1$bPIL@>L-%i#;WO}ek2{4ti8Z7dZKG4Gg^uII$2berX z=Vg-cU;)aCut@Bs@AUld03R>ciI^~~2dJGr3{aMZMV_z9>5+9}uMF{(bYF$e%+z6E z*5Cg6+fdeqMdm+O5DH4M&_3D$#J8cW1B<|$D+#K?2viwa@j(IhoAvIgx{pi`1_Ynj zT6%ES%W|;D_1IK;Wj!0f9H{lUQ3&Qh?D1n&IyaMrfg6=xX#g8lVQvC@{6AF$HDVk9 z9LWOppJ~0-Iu(Aaw&orHzslmXF z0bCax&Z~ic%LJ+~wkjXU+F`~5$m*^LJySza>qR6l zfJTawOV)HnWaUgC1%(&qfI6O?Tus3NA`7`9@_9fGChLMRaj1ZtD}9ONz&{%h#a-Br v8ZMT77$c{>+oMN`Wblt+Q0<+d?)3ZXmf5w#hC@4%rSM;33L$?3`-J=#eWdxs