patchkit-0.2.1/.cargo_vcs_info.json0000644000000001360000000000100126430ustar { "git": { "sha1": "95c0f5427ede5d581203c55461fb9633826cfd12" }, "path_in_vcs": "" }patchkit-0.2.1/.codespellrc000064400000000000000000000001031046102023000137250ustar 00000000000000[codespell] skip = ./test_patches_data/* ignore-words-list = crate patchkit-0.2.1/.github/CODEOWNERS000064400000000000000000000001661046102023000143710ustar 00000000000000* @jelmer # Release robot dulwich/contrib/release_robot.py @mikofski dulwich/contrib/test_release_robot.py @mikofski patchkit-0.2.1/.github/dependabot.yml000064400000000000000000000010351046102023000156220ustar 00000000000000# Keep GitHub Actions up to date with GitHub's Dependabot... # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly patchkit-0.2.1/.github/workflows/rust.yml000064400000000000000000000006311046102023000165500ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose env: RUSTFLAGS: -Dwarnings - name: Run tests run: cargo test --verbose env: RUSTFLAGS: -Dwarnings - name: Check formatting run: cargo fmt -- --check patchkit-0.2.1/.gitignore000064400000000000000000000000211046102023000134140ustar 00000000000000/target *.swp *~ patchkit-0.2.1/Cargo.toml0000644000000022020000000000100106350ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "patchkit" version = "0.2.1" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A library for parsing and manipulating patch files" homepage = "https://github.com/breezy-team/patchkit" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/breezy-team/patchkit" [lib] name = "patchkit" path = "src/lib.rs" [dependencies.chrono] version = "0.4" [dependencies.lazy-regex] version = ">=2" [dependencies.lazy_static] version = "1" [dependencies.once_cell] version = "1.20.2" [dependencies.regex] version = "1" patchkit-0.2.1/Cargo.toml.orig000064400000000000000000000006331046102023000143240ustar 00000000000000[package] name = "patchkit" version = "0.2.1" edition = "2021" license = "Apache-2.0" description = "A library for parsing and manipulating patch files" repository = "https://github.com/breezy-team/patchkit" authors = ["Jelmer Vernooij "] homepage = "https://github.com/breezy-team/patchkit" [dependencies] chrono = "0.4" lazy-regex = ">=2" lazy_static = "1" once_cell = "1.20.2" regex = "1" patchkit-0.2.1/README.md000064400000000000000000000003031046102023000127060ustar 00000000000000Parsing and manipulation of patch files --------------------------------------- This crate provides support for parsing and editing of unified diff files, as well as related files (e.g. quilt). patchkit-0.2.1/TODO000064400000000000000000000002001046102023000121130ustar 00000000000000- support applying patches with fuzz - support generating diffs + myers + patiencediff + stone - support generating rej/orig patchkit-0.2.1/disperse.conf000064400000000000000000000000461046102023000141200ustar 00000000000000timeout_days: 5 tag_name: "v$VERSION" patchkit-0.2.1/logo.webp000064400000000000000000006626501046102023000132710ustar 00000000000000RIFFeWEBPVP8 r+*>1C"!!yH cO烾}YoMoB$D _Oo_ ongvvIGoWD [oG\~qV~ ?xOsϿg#//}k͟KG?_^'>vG?Wo?b~:~A>/?s_~_??Ye?wGWGqoj?ǃk{߰GVMWqOw׼cA޿t7|>+7O??=O_=ODSO U[_o_=FN؆ʭ ɳq'aCʘsj;^uS+HPj⫉ r&F!Ui\Lر(cY:~&N*ts$|k?l MigQ]1ťjVΣA}9}Ǜks- i&B2a8DH9Nn dVl5LL@ R&ܥLYJ-sG{QqV ܗ)Vߖl'(^J~ɋͶǎذ*HW)8o!!40/omH¤~i`Xӈ3S6%VX_ߔpRm L)}~{ԛ5ˇg_W,f!<h:䂿2ԯe}kh/meo$a ! e(\kY2*O$֒sFGFOw,Û sSaLvjw\1%-vE>Ak/+kL!TggXE:Ӵ9; 0e&7?ϻCqٷ{_s/(X*^l▇ f[>^Air>a LjNh^J3K^?պI p. =l=H@p6D噬 V#IV_ ?cxè:FEl>5߇ S`<N,v׷ ?=PG*zwiK r['> $#̴c{M̢cjtUlSwXݎ3]~qBs8ovav7>'P?2&Ԝd؈WH^z][ZUU^IېH^ ,k4Tl,8a/,/[,4Y-rV"+̖2 TK.5 cgh /2{;]@샜I%WM\V1PYIriȨ۴s;cG{kNsǸm4RO/'N 嶷OHa<N*1@fF>$y 2A|DRb9~$8Aध-$nҼSγr̛BkW 0!53#t=EZH5w> |)#XXs{#>؇=7&*Wf1}f*+b" - 6~ғ67%AtI#1VuԻOf8nw}P9gWΘ?oi-L•aXܞVzi+W86R @9Q`~x~1*Oȶ{=GےXS M*4IX߫Y%W+V &K򞨿~I|ѫ"Q фsA{J#,5;x<8Y?<:#q3UlJU+' zg ʯt^]?و扊"? '=x2a:J?? ~X>?4@]U,D uM> jg@e<[BFӖM>| FQT6`չ+!4o3btK"&?2'C_Ej]R}\}QY$ pX3߽/(X_ckڤ~=Yw--RV`Y+&USXiV_9皣zMbk߉ }K$_7X .E]⸎鷣؏H~ō`N.~_,ZS"e#֑C;!O$/|9 ;ZJnclA{î)~,&{Bu (jx(|D5C!R*n1? .'-X^0j"Mj*Byi1zHh3-Z3K&T="ݧ4]ObFzX0c%A`/.Y`lLeSu@أl614F'Z5 Z4*fו}H ʆWﯶMVzaqfwo,vrs>oggm .':h+1CKzus˦mP-CڐS\5Or3sd 3,sZ"/˦i͓:u>PeoCuZX]1\@$J*܆gAU[zRУƉ*\itV`jT۵VoYbNdtJk۵YvI<fj`݃j0/DcdyZeП':%'tRlxk{t@zJQVG0#@MC  J>%e^(NxS$T5%w o "V{|.i M u|C6וm&CDxY]Uwߖ_P&+.kvbɴzkHcTrLD̖Ǒ4e̻iJL`]ɍThxF€w@w`#ܫ"9ndQAp#Jf`}js>;aI{>[FwZH#m;v*f*W)avd&9w޲'1djfVIp,GqS)o(2R0F|/v>" V2:Qe#?KPX SۓS(\}Y$f$ãMOG=_,A͜TE}Lgy-5սXr| Wy\q\rIXIRt[*RW`1tܯ-ޘ|MET4xȫfŷD_9 gVхoQUn9ͤX_iV㖬&*#@ hC >npiAN"tUF֑QKt2GޙKD?1k UPMI6y=+4z6R;pʘeNw;Cҹnz }T6(mOL" 3/exfZkN/4WE| X'ai}6 yd:L·s ms hQU?OvV} ߀zF~Mlފ[W-քDLH\@ ȏJ_ix-r$9:͸ʗ&N3Y9Zꨝ2 E+|DoO1r(ss5Y\㛣Vc"`BLi A\+ xǓ:e婏FRTKwfK6 8,+ p_^y!-_^N,N*pV=]yF=U(߻hVlejJ+5r9|ΖHYcK!6$)# #lJNIG7 A%6;|f"H kN4Y'r(".#U^06}֥=ڍoϰtAYtP/7"JXg)ZLG7o+4O1!^2a }G%)s1vPTIn Rbv;]I4۹]&?l] @gQmQd8fνvdml&GI[ȠZnrsb hd|Ps"EtC7k<ҝp=#ӫw5%uCUPG#E0o[^%CkL5i"Ig` +yt0vJέ p٠@8l0gm9@qL. p;ϱ56bsCyR'X/|t7B"dW,{u㺻tl{xLt|^XR]MtQ`jm oq-!!oR5ojbޅ2HOJR@m ÷!E%I Sʉb/DN#UQRqBѦ3>bZRO})ӫd:Gkc#0#Kl /]]w:ptx0yM-5o%UTϭ"4P۸&o6Q+"Vh=hQHusڈa wO h+"~;Y+둛SGctme"B1\yu,HtƌML,sYlC*ܘ3әj>}4E6< ?֟zpꥸRkfcC"j nğL( ^]=a;-'p])HNg{u]A5ըqK#i[71$0BSo<Ū%t L|Fe* *x&Ưl3$5E K,! $n8Pʸ@v16TUrzp7AixE}4oxK󦲵k-xy < _ie2=B6'*F͢L~MV7tD^K*CD芓g/#&#@qYGF$Os]*K!}^X2"ɢ4oNN{v 4brzII7*nVv r B7֥(y㕝."뤟7T}W5P_hQ <@KMƦc+ YCJW[76_uo[􁉵bGB)Q%PV槕"w7/[x梬F{Ool \K(\廡|&S's d9h)|z/E[h7l~`ZoYA'ʟ({pB[(ƫ$JQI.Yg Ǝ˿$c=iR"ޏ5g`o,LS3u~ӣW8ߣ ޽{wjP% H"U1HL'%VWZJ,1|A ^ ǿ?/5=@c냢0T*%S5HM .hI3 O{ӯ7V-%c8)4;CM*aKr_]DhampPB//e{(HjHGg]l>ȼSmD$m ˁ;o,kBl@!RkL'|Q.=4Ρ.t9荄9k73z1iwHu(T/ɘ.᥋ [Gu!:8&(Ұ킕G/@'Fi~/DޑDcA˜\piŃ?jtB)]WiXh3 |X`y8 Fͳ!:JUh1Ǵ"yK[TTHuze,_ =Qi <ǴR wdLjlFL.x 4-7"P~G6]E Ѐ!?$EZvOҊjE0iXP,a`wWPfK&iBCBXu.'b珮 2g}GoL2rCni M?l[ e˩c`sY|JHvLtclᩨ/l-q)Ti\J>GMT Q]%q̀__j9^tS褀O_w/rl6):^Wk+Y wq:'-D4(O ΈBLyYJU•ɐ[xGnsP阰R^(>Aw]|SUfB7VIꦤ#זBICi]͔M%-[J m fOm\DYg5kcM|2~=F>"Tmy8I`DMKp-t«onX8|uJ9zA~hRsAwR2(٭ϴm7Fr#A%qh6w} bs 'xalQ^cit@WKc}&R܍S8yL|FF5T[B@Tf66% W$6~ge3H,eQlR9RfK+]Ѓ+ 9N޼,  Bf俻_sʙ+%8Ss{Z;p;Np4Mo*_|'z?L-oX?cp]MPalP CJ$AF'lz0^A n*uva j$U8@Zvuu*HLlO豎Bbz0^A3wc5tЧp0 8h\Bcz'ꤞHa6$G巋;[&QŸ3ch4ТHesѺxPL|N~VU,N!Ǵ3 7 ]=YRK9%䰏IRм:|.#TQOyG3߮bAe9/O UlR#+g) =6adiKה[,Q K}\UVpSt 때,uБ$$J%YE~8cV]t┙ҴeSTe1( [Xm孶Layo ~h[O}&  I%ݑ7u/{hcмpbc=?vz0%0h>-!SPUMMAim8&E>x7"VugL?!IŤ9EH$ZltfhwpP>[P`?#C3f}W V w .$41\-"+JP%uvlc0iAjQ9r鬉=bOBؿ'{$vmoW;YJ=0#'$e`(s ay|I{&ES"FB~}bC}as@Karm$X W"l#W!{ ,; É[n} bs7ɼtw4#3E,EulH]w7955|rF-1+VSŷ8/[4v%&ί3|3)Qy?2I'uodc/N+<"IlU`ci_|Í͏" "Zn$Yf,l?>TZիWCE+>%q&"ļ1h&W. Ý^/('u' сCg'fH||Ew:Fgp BEw@0"jk]rF.EQ'a,jXG*YK=(tݰ5zlϝw r+vC! >|{\C3!M0b34Rڤ~Y<ȧ160uNcI礇k~7QrL ؘ6XMD4RG߄?V'ֿsAT u-2 |aA uHÏ|Mens`FkJ a-`2ҾP"ʚnc'Zcb,U^M"[< dTњKt4 y9+w2#<PF}M;ӌanqzz4xe]YސH^imUd*aW~"tib dpO4ɑ#EZ [>i 9w1ZtoTⲷř+jbl/ -Sdk,rݧвyڕ"s.vƣJ$qR 9, K !LPE;.Wv/][j$1@c䙍Hk!Xy$Ud’:bS&59Y~Qy'HHԵ7]F".OPY5[*T CG$/z+n!i9@Tֱe#`3)]YE<}R7cM |(2}l |&ݱ?&4[SZFՇF'{䰘p"HoAn:e1e@2u y6`j ҔϚٵuEU9bc'6s/:Cai6g@XCLW6$;(G}&]'ת/ (y8#JfX. ne6c}^Z#,Mc؈*ˢ0h\wB-:?|46D*e[ " =5Lkv")3(P۹0Um\"a2EJ:V~Ei6jD`^^GJ F9KkǺǽ$"C]@\IN lr<$;fe!kǜGT+}F9}â)8\J(@Q\^_bs;8zN Lq/"+jֿQKO ]psR};F JZX9#7?EW ]?<=y'O2t,%j8QU]D΀=e4.@Y>Ok8&JuoEl|,&zdߓ_ᬞזs2KЗɤq Y㽓-e&k$b* 9g{:c}yThsa&*-OrB,AvfQ{.?q3,/D>VW5= '>HRh“Q"9 ty\mOv閊/N؟!iuFr,̞"֢@5ќ>q1rl_̷}5f$1>v7ͮf镙\6?b`7zȁA aw01Kt+OQit=X;2s+Li%IPk|ꪭ~ˮB#W3Z6U_"/[8Qi.eCPptaUI{Ia]D-V+HNbQݮ4:ގuwܺmd;I(`) He<]~pF0F‹nX ~Ҭ8ʺiSI UCx?5=%*k*2R7iv b0mqJQ./5yWGD؈mqgE/VUsݐkh8:Th 8c O$GlDI(5 ɑ-{c?MvfOXE\ / ZJL1Hꚯ70ϖ+NGc(K!eDmUHv\ uĥ}Y Zjި ;o@LӃSeL/| t;6S_J3#Gk7_^xku$Bi ژ&R;DXz)Mkf\DhBrj WWFmw k4ùBRGoaUOwoj%Ɏ]R.;na9pRsy0nx`¤8,! nPpX$r- erx>*JO|_S4ƢxD<꤮O#A]1$$v{slysڥViqVoxdXG푵>k3 *rAV\, '64R-N~Dxʳ&Js=:;>XtGx+DMwvNlY#h :i*Ű֮FY.k\t-.vN ԳsoJ2J B(Ԙ6\m ߠw4z1c 3`wWeO{ xrIZ&A #Vs%to"Ȳ6o)XшlF砃Eo Ý+W%1(dx@$e)95.8~nl=13ȩ%΍~Ì6mvKsC>p&h #|!9'*B]یͪN=x ~B1 䰱 _iEUwhq|:dJAz̯%Qbtוʈ|]3U0x,w=S }?o0s#Yu0Ϣ;wI+//az[TtOvHnvxQ*)PM{i2UlP#@.#blRKIH g b}qžep}q@zؖb#\n!gH԰ⶴY,`X>xhOހh6%}/ZisPY=?>h y$@'ʇRz9bwt=|0TsCdm"mhnk4%5ysKkOg'U=)Ei1{mXZ"i 3p̤O'MƉ ͼ77$8"9'nj H\Nn5k q\;>xCܸӥZ~ZKP_|H!vaڭqk@6]cXM['$aGCS]+UN.{^ZtYټn'!)Qbx ـZC!.~wiaU?SdzO "=أO'լ^-j Ȕ=*sK?NuXW82ʹ!g1c}MM{LIċVtSTZ'zx16$lG;p7jP-btjc:6 q9y>OFt9!LJԨsXYNXς ?FnM7nd0]bѻDsMӤgm*V@%a'h{g7$q2y72\ӿ%tQ_Oųb?[ipz!H"9עԋ.ܱ;舍o \&^5%=0 d>Pn24+8۞D́ ' 8zgB|n5MY=,GݑNpQ% ^8b +?>|, j&5~+r7T _*'C+Lc6f{H<7"cxR6& 3=P|.7=1|"g Ȫ2iOq6g2bȸBeN/&~҃ dt%tŖr p22HFWh)F<m+k1(~)lq MV#n$M+SPG'eOn`P2*N4s3L(?uUB@t߼bcʍm%M?Ywp/Vayc{69U{[j 4d'Ok*9pB؀HrDkO0k)#]#QACEA /@* :iS[8g|1+@=5Q/,-\wd dٜ-[NR+- |>˄L;хLhd{0pG?P}:.-(Hf|Ddbuw)[~z) qlL' odqBsI(nVU) dƞ FL9&;Ő) >\|(4zoJu! nK>_߽v8F`M9\bs?,0m # g!'kT!<ƪ,rΫ?,^1TtDWd6qU3j4X֭*>Qg0/_[ (2.pX Bְ#k{1\@aRLjN1|oorĥF/!2gPX*6y(-R-6'((kR(+fo9;4y^7y\cjrh`fWʊV^|,I>O,,S&Hԧ Th6(&9dr[Q'y- ʸ\BDҤ1Xj̥8"N?)76{jx -7Ch$e]7lN'3s6E4^FVcSH^OgsK4QLxTI~فi[i?̚7^Q@C@TQ? R 3D9z0.Lj5fwrmz+l~R-;)bj䧮l&g$+x>Ld@}3hF_#T7I[QrŎPc\6䍄|]5{B:uPk,*,Q^QMjhIL۶hPѿxӵ٫A▪V߫YtW>3\r$g=3[s[d8Jmܬ#Vׇwvh( *H*A?/||in =j*?-鴆 nZU% .w#0Dym6ԉSwnTjO` tUJQtTOZkV` gkSIN1Uiq`T;z[ƒ|qly k"i e$eć].Q%E͌`Mt EZvo-+l}DK+ΠZ;bzi+d#NYtɛ~lh18EF:!7dep/+6aeҜ:h86njţ5IIV㮫+*<6o*y!i#'oAhߞ@pGjUԯ)]ܷwgFR/U J JX>_E7MiAD&:7YCUۜtjY^.љr$:#:}T/#UIF/U!Q!&4]ieWe%Qں;~HfݐL:Yc` Y[AmAUjcD$j< <5+D&$`'^|F]k7fpC>eGa$/5*+tڸ#Cy>poѰb99vbO{@`&]b 1-V+&w vW xRӅ hSJClMf'ߓvDKQc) &)tcUA9IξҊ1i{h!HBse* PqLmVNq?y[TsVjrޓCg-9* pRJOuzE) (z|ݒ%E(eo×h5PgLsG*ϭ]Uki`^]?1ob qy9/t4+Mn0t>l'w$8ĝ2Jܾ; >GHiO1TX$HhHc5z  BaPL乸 1D@t*@a'P~}ȸa X&v(r۟|fqڹ:W?J2ݱHHA嬅K2 傭{"R"k0qƩ5#"nKm؆H49ё(Jҏ fUF`/Ee/^;gq%VCb.F8Gp;cDqi;G#4AUHڂjY&jn6KȆHr: !p R:05)+` mg{  R_@[&pȳ*p~{K5n45)WB5e AS`,%Dn*"nwtQ|h$d]crqNo6gK+w+( LYGXQ6)EJkTtf- 'kGf!mږ{4=t !FojeE)%oMwa4MߴAZ%g'yzij' 3ƅ31XB];6J<w "!`.H^[T`;F5S-껃q5O 9jH"KM"`83!}R xl{^&7V_b(5҈w_ (mˉ9AbCD- Y6!A'h0Sτ$z]܂ ߔIO vRԠU. Z0m]ߍ9w?X^JY7ؼZXRע6sMo|S7emn8~_i' A:G8..&.k#Q} p_ P܊ߐ` (R6OT~ |FK"x\$eqpMA~XT %pv|[*b[ PҔl\u:;G:KtW߯/(C b$n^ c`Q:~D EEDZnkiibe6ٴI㭷m(aVǻK~9q1X`)i9KjL%G9ˁ~RXr 6 ^b+|(`dB͞f6tx~̤6x ]53@Ȩ#zN2:k}>v4s:j1^µ$<ɷ7ցyKoRUVe㡷/υCÕpU]O8!3Q}=ouone0LhA3ЖwE'+b"n1 ,^ @ )\4MaO ݯތ1հSLǎ>#c-!/ $-iqjdXx!Mp~n>Lswhu-,JfE:pM=d]WNyYd TB>ZV^XZ0!8!(`"OT,>Jְ6%x"N?-QP[ kGpT9dntŎƿr";Q)Z:%O8g =Uߪu1(yW1 xclvLWhɡo8[Lu$tt8ӟU/M/)b3׈i0jP:`I@^sջ-|2$! l#99t2\PK]Iy8s3y k$q- F%vjH*9;@~Y|k`6= xIn/)5tY N qxϼ= ,ˍˁQ"h=A0N,J:PȅA!`K*2Z&SHA3ٟɺvdBwƭTm蛯+?#4h4OǴV[.EH '%O11Sʭ*sI@}'KJev6WYw怉xD>PAg*GVUL󥎍cK=!|x`'xRPiTBgNvW'#Fe5B>unj.=1`)@&'jxJI/Þțf\75 CP+=a+ K!;A/w{3\oSZc3nfSE0g7tBx }tļ~"F  P8L`]T&N-KޖDvt( ?]v %Y 0Nyn;i}s~vAse=-aBTHNEh{?"+֓p~9 :fRvVotKd1'1yv 8Eu.iDly1EmH}?gRcW-.6yF B]0lF]γƋ"ta+8,AA;s3gx2Y"ޝ)mq+Df,eUoqeBo($k).b!hx2yr4##k}' A`od e  s$rS>NA"Yfn;疳6o")Hd֢Їn۷<.M`?yf0 bf5SY9/xMW7MHc^6B`&2Dm? j =a>wvB7(;r؞i7{R(N%:ux>%j-6BA`B{3J”24-?1sԸ 9sV>3 w0ʴJr C+<%2&h۳8mXj*r]1%TtQҵcC|,5 5vDk‘e>![#UH' 4f4Z]Jm]*([DpVf3qlP_wFCF$۫`ϏoS6}uz.A 㛮0 0;mXf{"޼&D8Kbl3E5Ȇm] #Z i`9,ɴ貨'N%:GG#6*'۾s^7Qc?4 *7O>KSԦgQe\2@Sch_ ow+fg_ Ȃ.)JP]E,y\ّd0ݛ^cn&r6reF@Wz[ʠ4VΏfݛGm[1S#i( \^Fљ 2Nl2<|2U^q&dVwIO-b5$Jٹtmq#Gƪ%x "W.5ӫ%ce!0\^~^GNG<E}/'h6؊G:+y0AuP мK: +$u`r}eWd~l >;_ :tշ2ܓ=97tm݈fS~t_]͋[)U11hR'gr ͆&h0bq!ʣha!1̈K~& Лjg2$OTU[s q)w3/"A˨747Iwİ5eB̧p۩3VX^#ur:u f~_c<ϭCcDJ)3RV1XF۶2X5v.O6NOFjS~[k21y?TbMA 2*AR=<vNjr-}{Y#&b[&÷XvGth{/d  6/:v1Uw=@w,-Ɛv,BUtU: LMCPx O-b"14r.~UueQ} {u-nDLcl/ω qy@34=HU0Q_TgbZ$eQoA,ѭ4qlږ#}BF uxv*-du.枸!_/qsFr*YZ-Бt 96-*SiT" RmeOՃp 5=&,khӟ6_Wd(/ʼn:(cUg< [-$)T'V6H (C#<}nN ?L &gR)`Mاfќo,OrY#[Bի6R;'Yfڽb5V ץ"h/9]<[,;t_{;QW~RB40&&Fp3C Hm ƅ?CHt!cűyWWJfEI:Yᚳ B?*fʝװH.ߚ7}xT.G7m|&c yNnYٶZoa e!CPAF4"xP$sb I=!P0гZe[TCw#)C[TV*GP/eJbӊ#8h@Ŷd)bgkQ eڰYF,p'ŏw|/%^`Uu ~}%ZV(Õٲ¬227+ yn6IŃy\N1aj\sv;`CF*aNPVkxdf׆}'nzj{@&6!ba`IAfk` ܲ7&b8ޙu\JѼJ3{B~n$ "T_wb|T_6Z(,:#W|vK n 2uٗj7kvmy/-qZXYX_3'Ů4m =P跼r:k3/*25]zc^6Ry B~tհrx'M>{Ykv:j 'Ĥ]/_V((vxܝ2.&G%([W$(yKX({ߌкZ5XT]T |f2O 3P}+ brKjA^Y;t@,$һl6ъ+gڲx-+aPcv|&@qjսU+wr*]ztrؓyS}ԱzBl<@vWQ ?ivZ0TJ>]hwj2rn8:'A=kסJ .rl3rv@kgRy=Gߨ, iз#>Oqukϵ(Ya DF"0:-yȕ^cO͊6%J{^|A8>.#_E.|J)jY:KJXR+ћv(t_ @7`Xu'@R\ ^ k:;O@"U-~@}o ц@eIZ,1XHvU(tU?G=0{&٤FFԵ*8]dC2icCcze=19;ÛǎQ" 2hpFֶ h.nZ֮̔SVSx_aЪ}hp H^ 'FU?7_]:K=8ӭ(e zW즩gEq\*$o#U]SO]zRFf-LUN Y8nCg}_vԼ 7˳:{sxk mשy%tE4vׯ =@[a!NFuEt2z|eJ%B.M?\?.rIjIdb_gt5KuF+a6oԢM=4'^p]] &Ь}$|}&בg|XezZ_Jj_cqm:߀4d{]+w(j6,WX_>;w1߫)̩X5q P`Xp _M' 4oun(|nW<w +8I$V0 Rgb 4eHM['يO?z*C*&䆆TXrP>4Xpژ!=$S-_J0ژ)c01 VS@KVP֛Q4Msܴsdsd]ucSm~nȲ:v&u>?Eqw %lk d8{ꏲXEmEm#'$): &gR|Nl!$'bkoFbp.,)M ݩ%![ fZn֔!Í3\~?PʳW OU64 ɵdĆ 4*)^RóC簯_Rw+ 5LmOiStxҾGlz͛[ Z&У]"d͹eN5ڲ"R%l(J]h;h |rsi]DDJ ḣ LGD.ʂ1j D~=,$tU6$čj)f0+X'\}hRNbU _BM\ܾww:H@4b{Bid|bz,)m>5kZS g!ṫCzͧXp[pŋXʱtގ0$L)3]"W_{(F x]y1B$8~z]g\YK-GRgHH[?q%ٶ&DZ6wp4Yr@Vw>`vϛz, C>WZ:@LͪKV3uvm*Ig7= # &% n ďXP҈dsf:Rּ\ D3p.Jy9r=EwѼ؄؆FS6`X,pE;]h?!LEvfe=/T9G3UGSe(hրeD>[@U(I*u^\d)Pmo+_/ftRoɒ\w4)_[B7z D jJoq/ݵ 1/4Ɓ:3X"<HE,޼$T)#)G4%Ks|LPN!N:O5oN %DqaZn?'bTƣ&FȝhvC8W<_()`G%6 É-zaGQf}ۈ2{JnG w{'HsAtKs0sb=Hhpd{u}$,%`qy{};[XGJp/1Q6GGT {g7jc'Ɛ&c+G\`>$tTZ N9cL[ C{O6d\nDIv{.J~O܎݇Mzܽ?&9ZMpKЉip$@o'CuZG Yq^VWKt(VSLWh|LѤC n:M4%h,)~aR*=j P~h[45iH&sm#A˂POŒ4)P9ז5*K$q( Y:p&5:Sw=B&dgaaKoErS"X] g!'a Ѓj:gH%dl P-܏)+s$Z5PAT zyTb˃EoL+rkwMWMgCL !)A^ N-zlY0jJlUS|6!&~ovD(wJ y-cbk~`}|j,Tt}#WEx[(c:-`cdH$(EW\SU1+C/8Emǀr 6xPk稣goE Ge&TнmME u]`p<Aj5k# De7ZCPi"gƲf V`NMVQނ,;~mm,+oMi9eS'C+] :^n@F;[tG* h qDq'D^TDT)Ef'6#(p2,;"BU9ijsetW,rߧT , \zD-L 7CCjhu]>o\cU{~`:j,2gs/E>1%}A伝`P`UMo­ 3[[NoN[ e^oJmᲠ; ӆ,vW&`OZ"#5~`T``x '``j37N13F^*ye_Pj%2 XO=ˌ1ҫCQ nJVgx&y|co;gߟv3M탼 p&z奟:0 B^G&10ڍAZ#xf۞`bx8Pj RuG/o ;,_O9ofK-~/2 ?8o()A߶؜r Ѓj**7j#^*nf&4eI*ɠ ؊aۜSXT@4PI2܋_yuQ޸$@Sa,+vuFQ؞]PlˋL--؏,̼iRuv]i'+,O(#2jSv `F3A$B@8Q ~TMq\Yѩn>]){Z4.R1ڷ[#=+;c~A/̜N+cΒS$_Ńs'; v7!ؤ#PPT=ՕW:Ͻ_orhdFo](^tʫeIwGlYA*/6J<Ҟ,yaMuvvW/eV^V<{F\wVTj@)]TBC)oXE34>}'e ߠO{3 =9!TCV"Q`F:g SF]*EN\ճ;yw'V7A͢QtW:zJUQC>:f..@[œz%8 Li&ɀ) ̠Wc.'VkQR[S]@&Pan,} oLd`)IukXxu oڑ1׆Bѓ7_bn 0cG(dQ_T>rAŬ؀AyDi` ^;-p[ -|,^_2]sRC1Tz,'-.ǪشR0!,o.̂/)p2E"JbTv.} DC`"{Gѻl{!yZKN(W4 ?;7-Q:o[O0jtg$}Cge˖j9+@WJ'iD=KE>IĊ[V{Py 95,Kin 9"jD}zbPds*z~bPAlR_^Vh!܆\~8!L~z虺mC񬙫*ECRp6M`oJ43Cy >+FHCਲ਼U]͔99F2X%FoF~⠷!l\!ɞ"t:pSߎƵ bu /5[rQ*Q p@ pBcM?SDzc\$~@jg;F"#&i=@<2@x7(2DAW*KmI =$9"",ֹ֒6z)+!|rŎk^9le:j7TV?aT o`aMp8ꙃ実L#],|z m7QNؠQ_ ]pZGj#!%.WFj z9j#5xM|  0|%9pUIB_ҲoerC n|zh{_di;*GK6\D.p' .;C_hIeZH\4"ʯfS'Q"ƔfC$z2^mi,n )Iț6K:_dy87e3O"\d*SDlzYPϊTNHcFBe5=HчM~37\a p}*.3U tN(BtF@iSbo*n*\!ScBOLt4/C &4z>"hñڱ$bĨ_\eῃ.jR˱E@5>A v2fCMD5҅Ixw>I|ḺQ9##*y]vwͤ iA=ʡJ,qp|Ҕ#AsY 0[+^oȪT.cs3g`#_ɓj/ z' _6r 2)! *~FOcCa %M"AZVwϊB$ٱdbW2b86?s[׋qrC?aJ7;l[~~kȞ0z*Z (im'LJ0Q6oֿf%]OZ9&^bSJ;m_ph3%#T@wE9;Mn?} ZWL _4:+JwԻzF$;⩃;U棧_Ч p Yob,(R7 <"EYQbzf,R< %yoWpp[VbљҖ\VozM6w oW?fn%쀒/,nq,CWO&9Sj"'rMěy5HJ_KϳCpEiT-YB^GjQW G'f%&"y{3 1&~ oRcz;Qa\26"3)+8cWGd0NtkXLsx'n4 2o]#.n.L5hڭdnvRu xT"u5;/I2g"},C! Ɩ&$^z{^L@}Y65DZ1-9f=B(ip L_y(@~}xДsM2< wmiS[?K+' O|VB p( i_ZzsQK.ZyF5?(p{``ՏtR)L#]"Gz6*ʜPLSAO\Eܱb}?e+xwι{[QPoJֺ֤!4A zik^U&MP6)ZuGn>ũ]|Ulh\-֤tџ'l 墮*u&xR "A10(bR/rw&.Ds)=j5fQCsZ͊.ˡ.=qS"yt#b̐oMA6J DyrnQ!(lb5Fd)qЈEjoi A*Nj[Fnoz@Fv腩{_ԩfU=o0',/{>I?! &tڤnsَLp^W$>;k^7# Uv+&گuqҕW~6j8zl6O)_j^BeVt Λ"+R12޳խWZhcCZ4۾⟒dNb~6]8W# v A!#pq'-B8n[_ȖNy( i J>qb7}iEg@m& Xh" eLމmbz}G v;ru\ybhB|7Ty\zʋT)4ijK|cppr!pi o+԰ $)9+:NlX:@iFO)M[})cUJ2Y&*|?X %yi|)29N]iJQM_&豶F 6~,bU<s} H|?pC ׉,1XaU+}I|d!Ƒo`(r6=Y;4}hML FZ-ndNo+qԧ.ULܼtLK e]|u%($}heHʅ=-L8/P"** ,[!wHHG_Mm;"TZ,_r2C4[qNDIJF1 W7fG"zg 7}:f<Is@.2Wfɿeݘ^`gQJ=scA~;ݠr~~Qɱ07f(tZ͗6Ζ iL?vby@1U*e`c uwnN" LL-:;0c(@dOx%(!ƙ+vYZL\ZCNS[$fOLbեޕ&tAyN_N0v;§/QQBF.6 cJ;%iK¼6[#! FȒsHn|&}Oc@ Ė9h2j0"cp7hv`lsG4ډ5qb~]fR?lP5EL3l3A?U2ZQdo9uL5=c!ٟT BT1a7W$/7.[Z 񬕁.{"ۓ_8z~84vnڂ4s!Ѫl TMu7ѮT/rz\ j}sB3*3eEĄOqW``9a }SQZ2Bm[w'@P˯ V3R%tA=FWqEm?L\ZC4*lbikQ#Fbu{U'V>RAh&|1`#0Y\جZ >H'ѴX$92*,bG[gG4QK:2>SyGit=J3;-2Oc=A$q jq0 #OףH}gim>axfQȒ;0Bb@NMi.*s@B3aĻPL+Z;4:m`XKZ_UW G SamHC_2b _+WT;}BIEdK 3D¥)Gy@]iCjfsFniu3 SuwJe x`QĝZЈݯOLQuCmvxpA(X6ʔf^%M**|gqE&?40NnA,`,0.eZJXU2t֦hˊZ*VӠ:!< K"xZbue0bkо5$\~ 9IGPicf>O} yDI0KFVSYq! :|R6QX2"ˌh\,S*{7<9rMZy/7"@s 5.G HXm KȢejgaBqzz4;#nen|=+`7~llWL0 bUI35ͧh?p zODM$O)J2XH8ӿQ 8L$ -n x`/q-Y0ɪ4,rB\ 4)uJ9ج`jUQ1f: cԦc.o?]enBԧs?{aM-4\8# WxB*5dqμj56_@6[۵#)L_Qկ-I;HoTLh/SX,wzPMx۷O*Yt@ ÔŅEѩD= =bCڻ/mN8mEΈmi}>\E`VjDOTmimM3MB+$xTeq g+JN\d /r!KQ-B;Dy&k2j5:0Qk\F4MH >LdjielYaRp%03oD@ٙ\ݨ* =htɸ6Sz/ )r>A'K!Ȑ޲XvT/Ҽ9=hK{э{<= ܃6&ʀ+8X+GJ\#X7>,wV!Z;ZTxʼg╯yzEx6iqS˙WR8+D A9;NIe|x`+?p(ʣ!f["Qyg g"%.py\yx "pQxt˺{(6guwF(y):l#8"i^W9`KlMP#[uHz+ӅrG~ek ^SZΪQ9xGMl`ou׵NM /\= 5G+Bͭa9 "fp6|Y:mP=_mP`{nOuU䠢/d m {"yḆ.$~b?EysGџMS$SPt{hj g M?G~̏{¥mb Sy!9D,]CS+ L`8:RZ2]1x쭓KQArߺ="%|X6>hS5aeSQ_0N+^{}G@ _?H$1>M5n+$ YI Ekgǣ'VJ]p~80l|cz|h<=V RFv*v=std|Cp%vrJ`)ӓ^!7/pee%~[ ׫@Xٸh u ^ҹ /o5CC!$~V@+,md',HIrfˎMg¶}tud%VChJ([HQN<ƫx/ :|k.+Gn./rMŽn9t: `13ePѸ騑_P`τU2x?xy*>}CrMy%vCE H'ASaw(N׌(0 w┪Ԋ6"K\@XrL>621S͔Cji )@u9m=YK靼;dҴÄJPnBK^ lf@3K1H"Ҵ7>iU%pu+ԸƦK+p_;D4p#[B5l)>]+:ncUVo\}⽯2+q8hI,b~C"<뙐1zCȼT&zx@(,tیbiMJ>/hHht.C.3 ,`0^"q%7ĵSo2ƾްNJߠHsECLGtSI]cuwo}@Ӭ K4 0 g@z؋ (>ޑb+gPmݦhls{=~}=wȠL#~p3Um]/}L*PDJ_H!TF^] AH}eV/Dl!5#ݱcyޟ=$FDhҥC茫WTO3d̡TLű](|D% $%g%ntfZRVWѩu ȟ/7H+aRxDɣw:}&lZ\yvC be/w+yS.s*P(:6OL⬜CXPL = ڰ/-̟@bք{dSǢFӐ3P) Oq,.#-'vXH$m+L僤sv HH?s%M9J-t~քĊ$9K2{g?B&435 }=',}С TVCrqHRVR:Bl4Mxp!|8mZ)ǘ8NK_E 馢gW hu KwzP<얝2my2]7g#]+%1{usI5;#F;zO<ɳpy9\ٻcjeU*0Ȑ_b ܳ?KDD S4/&DQ0u\H\)>3=4 5+F^qn'Ofma˄/rln1;UF+n8ajuȕjaiq\yc"^}' KQdĿ."l٪ ]=D9zbkW# yW6H 3= q3ҷU\2!xFM1Q%x[xTO 8\+)UXqP'_ijA|: <lhrM A Z) 2O/[7ԆFIbpJMkh@f[=ip9J8[&:GwF=ob NZXKY \6ΤBM`*yQ1G޲ʫTkטa״?'[#:mKS8x&;0Uaq 3Z<4{rK?r_*45ݟq$#*oJ5߃~kc<DMca cuhvDAmB%6[N /fx2%b ?"" yn$vW?vr=2pwp7X26p (eϛ~ml~"ʯ=7*Z0۽56aÉ#F!:}QI6$g`Ar1Ն57ʀS.EJ%_F̊0AXiH[H/t]FIpQ eLR{}zUKaB:'&|f; |a"h ~Ќ<6ȩb?.P'%Si ,D#MIU&ҋv_)^k#Q GߵR;' >Җ#$MK]uX7Uq?g7x戨ty$@>ۄW};MF03H%3cG-x6H۞3Cp|B`R-UUTOrG&yluz-IT:UGU zu;(|?[!#ʆ+9YiY!e,{|`L,[h6|)˖~CǰKrPF 3Mбs0 N, ]29@HEW-5*g6$D(Z6@x5 KyƤhL[XCȏeiNDÛ$pS| KpY@nC} 'uk_ ff!KIa j֙Bd\KzZ3S^rPL}<͗* 3}# y]΀ Ur#6{k@Lm W5G'xWgdfV5Y'- .C V|h&%xԦ@HT@`s4 -_WxAT_ZJ5? T_ZZS&x;Sğ"X>&s7Qڕ~")oܖUOa lb2"ZC,Qw>zQm -3NR||{3g>{H@ԵwbQ e |< >ʚufWdLz~|:mˉ9:F鋬XFµ0&\u>K坢5{Sݥ+m3L<>hs"(Oe-~#0} +m˪-tF&oy{!&#IQ]Jm@5C/isy.+tCAK0OKNMfsB៚[QMq)H0@@Gjk04󁖸3]2`H] RZs\eCRH/X$eZdӥ؈"̕]ɖsچL7R}PWFb$KΦC;d*x|y !/\ ]"lZȤ ?b@*A, _HZRtu4tDBUx@l@篨)z[ћ'aJ aTٵ1@!ɹaK8)uGu;xhZ/s}9}owRPs'ϖwm&&Ѩ xD&sd_?1'8ِ)?;=%tIвs(NR4R}GH*lF`GQ']7P*NҖԏm-SjB1~$g`١lpMj^(\^§0UQŋ(q~ϯwNQREy5 (p [LݫQ y6U.'% W\= ``L#?;a;1F^ A}D h-lGzcU] 2-5OmB~vCg'r lŦys3WDS {z kQn WhW,s& A3˽hGm)5Fyr3 7k`RaG$x$qOb]/3^r{ZD*562fT(ox& zϸ;SzY~xL7VtG+Sie(]Q9i.v\;,E::;X+TVz,ldVڙcf:jDv]fu$#b}~d!+9kDD5Q#ͅv$5.:~$ l^ӸmP*zrD=T WSm/ǛNwb̦Y|E8Mmlsl8l(&v,ɍCىG翤n'xp久.Y4J: mDR¤*lt>:)4CLEϪev҅;5\rӦF+BRD?{sK9j G˲Jxo' rpRɗ\ T] ʦQ EK?_d|z TX6~qG^ a'5cxt#*Ɉ@"*3 d.O_T&ۛkH%T"gÅɭѸi5`SnaGO [2s+Rm.ﭑ@tVD6Wx9iMpd8X F_P2X“Q;/كHd ~b޵lEh8{=`^֪Cw4O3Z׷1FAF~;D[RkPBd>2}>!KrQN۫N"ru HJmQ0\cA)#l \(M&#&I!Ϸ";LlDNA\,?҃{+J/ ɇhGl9XY[[}:BumX9[6| YT lO 8n&SpL'oSꥣB VZҤK[Fg( GH V$Pa|Ԑ 7{[Va ̨ z EY5G S_fԲLPqhAn5% |H6X0#T N.H,G(_Q9nW GںjpILB m=&k ;6%:52[' s%I7t֏zc}}ٷ]L7Ii+[#\6,E|hy섹J`g?k8#տ_Ն=CMsIΓk yUn5= iH X~*[{#=ad+DU^qO2k  -<lw[hq3r~͑.,Jߺ0JU5„ mF 6e# opqZy.5OUg'Mn  |(7ymZ#5HU8Ӗ X.+>7dE13IX]U޼&f(P#XuL]t_K ڋ5&^ ˟~ #5ߤ0sA[c]Jc杠 n\1کRn4[&^7ݤw3 9KM9W, dya/x X[B-iX9K?zy26c^c;%Sb Yr}w ՘"Ϝ/Σv)8DP>,+"H*|c,8_[x#m3k0~X>9ps< #n8;mP"/ʃA cONޞпq*^kvUpzK+ߎ{_n;zd~h܉4P S]./eOke8ZR4eQs-թ!yL'GjIЎN1|4 'MmN>:GK/?r=!+Kœ# LRZjCK&(Q%Eu쿐:, {R2+ uV W}&w  &kQ`Kb~'-ҢpRwƁjÙ#0ݨ}O'VYĬFDT[1 {'ʬdi J#xW ~Q3v![0}TQH:)Xl.,a&C&賮26[ݱEON;)\(_'m +5 ǡKY(M"D?_o9&4'Á3]WHJYs5qyT-MO8/C,D^g^ݳ&sM  40HGn#C}Kc}bEUEDyDLʄY,5Z SQd: Eӧ8fL]:qnMeiE.Fdϳ2> i#ɟSn8xUVÇvy4m/r'^rorN u+[C@Q0ݟhq&RH. -Y+MCo"}hE}DD!M'gj, ͈RDL6PP3b iPvЮ%b2k# 8bO5lY-T'g?2Cd k)H/2=YAI\⿃=RDǍےNu`:d{(%m}chDi43 >"c;810PA#+XZO'1s_C#0P. Zj斡&ϵ%FJSA- .SLõս ky&NrA֐]-+ʇekyWq<[V,3_xܢp&n/x!eJ7|f2Mk?5{*:,F=Ov/q,Pخ빺čH.Ջf|S|E62ظP@'~y3U5= #}nAow/UUE2"phNﳋo"o]䤲/]$(ڀ9cͿi:`.Du</zg*3|MVS4tpF"g7.{e=!TB;Ix7n4$hh?E7FZ(s|Lh{/W'-eo~1G#l AZ;eSue X)vV[%EBuJSW.ư!Q?ҩ49mX:P6e u`, T4jGO/3!`^T Nor5<4J vB`g{1"m7)ɳo >E -1u5ml%NrB{=Ѹi5 T5b܈DɲVe&L0y/AaN~njѲ$ 3X6[fR )[|7x?˶[9_'ȥ!| ,srT @qQY~BBy9_G>w%Wk|G2,8kM슱r߂2Nb.F5N qǴ5Wm\Oπ_Q*`?s-UL%-$rBAթfIB.@b`/uV&=jم +P%%+^Y0N%y z'}Ip t#T3$' -@!Y}9b.~g>z*el:uco*FF|R0­1_z'TZo[i.7h#|NgiSWƩVDcɰrP:AXOH!H(zϴ|> f_(8CY"_3DE͜hRzel7XA y6ukx[K2;?)<)c]H>"`$iqvo>k @.N)>[(1A`27T,D7o($!#(ho?Dt#0'?'D<GfixV6~* "]eĵ2b <%0ܼc]80$;i79`oJa(O6/3<"X.mKnTR!a ,Lثj=g%FZ-!Ǣeh4ť( $ ( 0 I .)?=?Cs)GI ;xOq9]OFl.M09^@D.̵[ yMmJ|EizgNd)J~ڛ.0v.6X3^8~NL('F)\Ѿ,|-KB׬l6M[O\7B@ 4m:*sLC(a} |,F\ֱ%y D AXNqJR@ϐ[y|Ӂkܖ_ثqW!rҍ[P 2Utx?[sۑʹ~LgS v^.R ); Ru\šgp48O4SB."r[ֆ'QDMXt# rcS/E]7Oe%!y .h;ԦURhaĞϸ'Ua;Bomj93*vr}KRCz?@x\4!Vb9 [w|B~p7^-siAʻg+HTxgfw" ecV iacx^1DEYçoB0 2ˊe 4u=,f1]xL$$"Ѳ9_JhMxngEz /dr9Rv^8H[]YxEEݍJs1gvga;HjivfvN$4qHF ?j!`{Y^3ؑf~9D؝OYt CfELȠvD)@\]˵„~1= X@6f.O܇槲m ƐiBz=HDI*Y ],c_5UZ'9"CW'3hMqz !XR p]z6o h@Xg^P:f@*0wj*ZP[ 3TA/ ~4f,&:Zي;cO`j +b-m g2!s|ز-Wqd TezYP%+go:HڧG9#4J3Y/DCveKnKPH-2TcE84{:; N"Yc%BĝBܫi \ )8j7o X.i2W%@tozg.2mq06-g($}XλQ!a=97df.7UZ:Uz1w.'r˸ U MDF:lx|ӌIa'P^ehV?fMKUuU3#H* 'l~" E2sE8%x2w^se}d/(-VNY_H$Lz~Hjq Wyʜqc>*~Elٶq%+j/ɖ,P]1VȏMxNjI:nj1H5f7x|u<8}pE%3-C@,϶r$]:vuAL>&ª4'Ņ,<ԝvuL S#ԫ{E'J$/FdX~:\W5 k6!7'Pn?ǵv\?h_Vר?>!/ƃuu*"PzFO[EoI ߔ;nEb23? 0}FSBA6n+/)yB#Yp$m)=*үf.,P( HfGkk5 NT$C&['n?Z8\(e/gk'1h|=&h(כO5ڮ0Usӓ8xa(u05jZf~?B)AHB"rHrˤvAoP@y_wH ՋqBAݤZn̫O8FކDεF|2*V0GPAaN\ 1f#!{jP8٢*6*TcJLւ^S*ثl8aݤ<2OjVϫDe,&H%zJ͊|똔L j r}~pH \Zf}-7y2iO Ķ/@ p7`d@T/5^th.n=ѽsU"'d7/k'6v>oK^t ^6:䖘y%hvD(%d' Q'XjhMoƞ[;} ,JYq8~ x,S16ܦ[;X&8E)X<4^ħw3eo4x!hp=[)>uJ#l? Ij[BĹ;K'JɈy:ud_hh+_[ 9sv!/#-.k"F24~:`#;X"vc(T) L?UKOkn) U jev}ٷK/G5Tuʴk_Vxh)%Idrr M@@*w;tPJ]$eI+ym"%;IXoYUmjPA@iܙ!u5-WN1m&-X44Xfۆ)1߃+i~ݲ/NVw^sX%)0,uCNtc|1C~ 4u:JR&> (8|Jdi*`ٖb = n\xȬ]9{JӂG;4Xp&8@iΣ:$I  j}(O36GȳJN/E_5[cs'8MмO&E4q&!1sLQK4{ ~e0YFt;"JfČ&tDL+ 8zkMe=d[M ,#%H`DVQH  錂VDtXQ\9;rI#YEɴ28vHkKh:_ʫh۷'JSq%&]-GA84bG aD{w݅D qsXzbaٝap%=QmXQ X(Z]ĭ[M$pN jr`6k.8# A-wyfѕR`v0u}>W{E%^,|t>hT˃{jZĊM&%L [^TX',GFAOݍBwg" PRNwvbUCG?#^=@݃1?NJ*VR[uXm&SԱ+n3gJ@RP.tç(Z*"$"GGh3I8b*/Ju!%6ݐT\)(qlQ>k]M~#"OyBmw^ vSkH\QkEÄSf&2Ls ŷT]G:om~Umf*z1I}6f;B9*Q4~zRCs GYUkk=נx!8ܢc r,?"ڋ7;6ak+h %F ,6LuVE̺Zz6Oh98,W|Bj$g:t@LgjےR-^ZG7VHCO*LRS[u,_)) ̏BוXui}C 2R4>nL+t!Q/'"vG%Oa@ڰ؏%&2aﶥ)OQ1N dl.i/Y:)Ъ;1憪H-ԋNYaԼJh߻$|M5\.!Ucջ4{C %:N{kY|d${̓Es2QC]ϰ/7  "G)=@4jRJLѩK8_7" rqnZ_gm^#h +n~:ḏׅ^oGY-Twv4Y2G.DPHǽl2BObd FNKx4 FS5>G(?o=_6bGO{NO|Sd/ϽjtCggC AWUTS>oSm5$$$&bV0<;vP~7il;s;lHa$aJ&,*h}t$x[ Twޅj۠;(--#IeI>"!gؔ۔ͫaBK9 ab~h`0@%a@I=^퇸NAGp/vQe">S5Nb', >8*{DeG,["qZL1}m[ό snA b V^Cl;}|Z.yia,-/"Tro1UcVmIN ? gߵ)`\7|b6W0aoRjp#䖪/ރW/am`ڗ48>>hiA9%KH)]c!;cPG[ُ5rbSI&{4IPJg=5 ^O1 $"?j"-a,ӊe2`zM:SLؤ@.U[4:I`M.?pH]rdr ]e1X dTw4Q?4ew.Aj/M0m>{$p+Y/3윛8O33%m %&^Tf]q)ul'?=[ݼ%Sl MC@VKZd"DY}Onf#,W3Z yʎN qY C<"mKj#B7bAeWGɹצ 7꽓ߌ{ @GvhNf/5s_Zb@A8#`HQV~ ^0AJ]qy oɺk+pCU"e8HC[fG]|,+V|aᜌXh!ND%ǿaܪNuJXt#S@?;`|C*7TI ۥ.pETҨmnS[u?4HJ [ یR8!ʮ?}F}Ql !/I׊[}aCy $˰ԉ!¬ @` '|MN3Or)[C| BW8e#~^V ѯę ZX R 6{5g0VB(,3?PInehl-dɱ_,r| e_3Q:dI&X[{M$.noݦ>S sĒ0E:nß {2)YX}%{`5Hqh]7N+@#&,!Xo%}\}KReU\ s+u=7էxPؙIwuo$6~b"_ecͳJP}I{swq`m&@=T5zr=CP9%SHYPA%J?AZi0zQQ+j*dOMV!‚&ƝjVwwg(v_=s\nˎvܑ$}Ii7&fA A#8T/i΀!<"%* l{ V4Mr9ˢ@8 ;W]Z>D翍N9ESx7G?#N~W=p=20=%pR{bTtra>o1LHt>xT{ʜuޞA,]-GU:ܕs^y  !VGnh22핮 }Y ݤ9F-.2B{_/ă‹NiCP|V'\_W(8o We8J.-Yth9L[ȚriAm;;At|'XL[*/i:5T͟7Ԓ@J9^jZsktyTڿᏽ[̒Y}&- {bǻp:k}@uz6 ؟ޖ |(?j$lL{ކTM3L%2`tkN"QMmwuǃkYz*9)r&,mFM Ujo4rIcq\M6_lSɻc'?eĵJG$&c>ǼiDOVW*0F+o GuHt!D6ٿpQ g^1P 1!|xvϴ#LМ@@x!XƁĭv=WeYE$N?5UTͥ|ܿlG10Tjvn-jvjXpֲ+2.p&f*n3bFN€ 8+b0)ں z6jp JR^{;B һrU!S[-hMjhbx&C{YwxHN^E/[K]h$UfgX ,o]񗮕jg"{ H 'lQYⰨL{T԰H P8!{Z;۫/VCޫO~H݀B7W1f.MB,a&oA>pz55p6.Wq"Aj+"s3L|{f7 'Sm?2YNH_V2HmZ c@ }qbX ΊK4 dOں#$ŭTp[g#n%,!@hm%3Dʋ]J0e3=냭e>?{#5g]ݝC-$Vr0XII5"lWg 30 2F{x/T0 pd5Q.)h=z` iHRS:Eޟk]yRʊ'-0&oH ]P2kzPD+Dp$`Dq:穑Jƕe"/?l ޾&).3W_5"/p}HǙec>oJU6F7SCbmj_not[y*T X+TFbh>ql˰ e su1ƱnU$CBՖ-8rU#R(h[ڪezCTgh60m D:1)!~IM+(d֊%`V$^t~$u6!vA}2 @y o=DϞqvW&5MYg]˜>5,9Qjɸ֡2.g `2]-ipe_1[7-~~4J6QdR:;pI}! 9=ʦ]{YXqݗUN CxzH+\+XZr)YWK]A5'CȈЛͩ0dk!Nf17}{ TYǝJR UZ%f2=򺄩FL4j!7 hh1h[qMe Za6_7|(WTp3pS@FPK:W jsc,EE0J*`ۋg9`4 ؖ6aw AյDϤe ͏RU4Cz c=3U7~i?b7վ &? 9$h>xV.瞷u]7%10UR`#koa@V~ mPylY)t*G&,?{X+~ .aRܣZr];͊9,&f.,Wם;Rq'S{B|ld1뷑.ˠSꇯQhm$Նi. BI[0 ]tJ"! 8~Zs[avVmC>L4zW3<1hSxW*JM33ul(ܦG^ǎӗī~8]8 2pcoT=$q2g[U'uB yr>b<>r{8z߶vY'W2md֫Uhk1ʃB<)%k[e>]2l1O iNO\? a10y#Ǩ:Sf>u\w_=B.|(1sbjwWd ǔ>gf`VŇ$ힶ*$@*X"TM: x木I]$B΁ε*T"[nCON66wGՈ#`ځitEJ$W{5o[[@Rqۼd$K@;7 _i7-X$+WG.dGjnTْWg̏<QS :̉{dK)!ݼ!;Ha`i 8KH1zOģVd:M~@6'l擈ē+4pKs.AĦcL.:F$*"P18l77x _UހQ5rg'v0dl]E0YjI N`xZ4~$\L @}uh1R)hkGu$D:n|1Gl4WXN9PZҭ}ԑ4đ3X{KžuЩΝht&Ps x2N(_a:n8baqгgbAFjxtjOMkMRwJuZ 4jXwSлwsuSLl՛e_iSvPN f8M/1~\q,ɡ:`|A9Lc)s\:r-y¥Rݩ%7|M"bFG =r>t͞5z>gFDKNJٹ@s\'UQ\Y6 L1A]-zw]%zf0l}-1Y((wkt: .)c= BYKîBFƙ!W D/K:g9b$L_@0Xag x-孄&*Oh:W8KY[=ʂE絶MS `D;mk0vUƷU _^c3**t8YjхÒ )/ lmL-`;aؘIov\Lg@? 7cPo*Oצ^ta0ATiSZh&Kۭz ەd .&T/eɔ.^@SN#_ˮeT`^C*Ӷb w|Xَm*0AևGoNGMZ蓈:Ir'yj @l!o|s)V<YL)AՔ3X*~:ܬpgy/ cgvwWݧv$qUTW(K}6;hӹ>#봐/GGdQeRk@K8Νd՟^L;Ĝ|0 87a ?>ׇy{a" -_ 9ipQ]EApـ9Z+4A|UPwA"+i%.9M"PwƹXN$ZҬY|E,kq{:ٮ^R ;1 DG]vW%|0,/e'C^ :_$z Z'Iouv[2#/EYAǷG9ˈ+IڣAkΒY <{ 9vJz"9Qб\&Iz2ER<߫#¹& j5{۷уaH=oŶ\KoIOBmB-HxbktiuWˌgX n컰'ui꤯8L-F^#⺪׈r4H/nM`q1[u ˑ!LSzŗZQCFx5R$]HF6K"Z!3f|+GO0 kd K^eUfWnsC HX]tenܼ[1|M7*LAJEhy)\kU2)#+=T$%L*^ 'iУOyyؾ3ɴ7HM=X%U_,?+pQ*&MOwޓIB?)\aS4F^'x$nP<_Dm mE: 4S )!|ZYվ7 ;Qe'|BxzaF1Иn%q} 0*ŋΊcpZ@]h.fiq'H;1^6)B"/R+D>c|P/ڪcQthY'?Zbgiaٻ]m7cUڽ UE46W!W1PvP^;Fl5f$w]Am6햹Lx쩗5rOߗ,=dh٫Fぶ> hTaC/meSrA+>r1cX-{/椲=ǯ *4KL'M\71C2ЃϠ j:G#D/arrEi@r*s4cp>k>&|~iJ4$olI M{Km{g <П7OĻU+5=?[l;ultR# ԾȒp\]>" >衁?@uckX":Bd7VJ )%/PC8<8waY@FalZ1YhG%t#^:Y>VK2^$ u.`g[@G|t9=uV8>){Ed)ump2?s 7`k2 r:\]ݲVz+Qft(/Eytx"x}x?k;GEVËK%dDDTI:9;~&" S*EtT?mN?Ώdcسzc/a,Ej_S wfJR㕂i4ջU&zgkNCfsQ+!"&ih=zEĺ߅a|WXOI x4L v<;RԳM!41Y] V_ȱgofeOnPGUERvОLݒjOodyR[_x¸Sx1\fXĕ.uy烐5)fK$?uTFHE(*q^yy@.Co~H>{oQ#=ӫh>%?h9W2:cui9HIMŨ4r(NaNχW^RIt_eFWm5_N pG#X4)u3,ƽvyhqP ?$e|t>\mU6+8㲏t\pqjlPh.Jɕ=՞_"|>^)dtIضR6ﷳyv#뗿 ^Lή2\ӱq}j.R1em%EM)x26{ZX`P1fkUxO|j" TT Y^@\'ow}|TsNxof0zH&, u9~$~U%%`0z1 -K"B <=4zMj\-)(^Ln'^vˁZw>gm 1R݉gVWH $&IC8ùalBʩI~ e2a6JDSB\^"]#DZO\$UeZp((n;CzДÂ\4w{Sdp|\fڨuN}SeK{X\ȇ$*gq#/2xJ5*ecoKob4)ΦY`Iu.n@b{F $,4ҐJg4(_,A~`,3UF)?-G|Qd"~cC]Vi1!d'BnB#/_%kS‘/,Γm4*5xIS4m!5?kEi 'oUH;G`l;ǂqμn|[cԂ~:䌼 ;%4;Ke/5Aq"Je+#q˼ V<21f~L\јSM5<[M6#Ub5ҋ6F Dy_}כĈTyXV;Buʪ75{륟hG> hQz3C氽n)wÎ؍Y\ 0A:R0Śrk0#J z;ˏԼ1sn됈>vV[>{bcYaTC%rd+lwC*->nQx5daIf~^P2Ԗ+MVyfsF#8ACǿCzN5C4jz ;ptAx:/D"oW0Z9/| Kkk˫=yB$(Uf^7j|χ \¤#|nu]krֺrl9Ž`^wetb *PlMWXʑ`8\DiAGזxqcJ%Lo~= ;M pCa?bJ^{ؤ:P>aګU?ӘNT`gdmve<Beɦ\N;6TRX)lk G%kw']S#s̼p=+["C,KS$"OҴ:5jJ7AdEZPoݹIm*x˼:7/lڂ68ȱjƗ;.]%X}@;k7ʼZ1+SŤcO B5|2Kʼ7PEsҬ$KD]$S_e"  |g=Tvڽ~$9h_fA8czmۍ佳m$B_K,%H HNwtt=z=s5 _&!G1cj@\P펻!r޾Y4Xs۞y0(K>_ 5!prgaKF?C(=*HwdeY58*M!0~W<\(tOXΉDE݈wol (3xRW<r?4 cL(J[o0(=}2KHB wȗMsSל9su.ngwx 2~):°x-5t+2uFLYdU\t<?•6g8sSa`W%-<4H32G(p) MYlŻpyY6!^ntf*TD@ƯɜӬ:Mȭĝi/7x멮 ^ kB`Q*pFk@{NZ?:EД^mZ'G!Ld9L6:we`ڹ/i"V!F(<''>|Y64ENP`FC&“H f4{EC&~ :fW!k~ᰞ嗊%,4lս Z81OfIA5^4ݍJfeIqƅC}u׏v#HJ~2AA%^Ǣt> hD Gxv[wˮ?Ь-W@#w~d˜^{óB!Hp &#~ǀ<P?R9Zd#4^:%}O Y\H@- jX)H'*v 8e\pF]>D9_P>*{CpzG-x"6*? sZXqcRYu;ZOl.$+аN1&FR,5"Ds@+>_'Il>x=t ;P֧caIz:pY2NŖ7[ye+tDNdf#@X>E1WBu[X O~$10OT ^c= WgڂHsI:N!a/Yxϰ_ HkHvl$]FM͡~ %qg^{v1Dmq:4qB?d趱2{r`@yRcke6 *bII?m}Ā0tEfOycmQ^F|(CY>iic$/% z/ٷ6ƒ9j7hD#gxJC̝ dU y1 K{ u-ج{ T$ =)~RR$,1USw}u2G1MBh<‰JV*6 `QKhst&7‰ìd~@# |=t[y!a@ 0dkb ;HbhEqr'n*R2dg֡T3 Cx:#Fϋ8+zx2 ȣP:C]OJ[d]VkP\+篥ubH)+9~G G{.T.=w])V7*Em2NyffRX.xmʧ31R_=ޅŭMl2XZ0YŮFۖt,7uV5NrlrنBeb `lZݰ>Ԏ솈k☃2|^'V*` OdvϮzuұA`WKF5U~caW*W&@?0v3ukc_F.,۬EUB)>^-kmă!+2`8h5 5x s23: Cgӏ sS\4hH ~y*bj]'d@3.5mvfFa4u|QTDWR#mM\xV,c[>g0(̟_F$2JDd`$$Ԫ=c1S UDE:5z a?Tvv+u ~_]@ 2}Z=֑*;CΒr7< ^ EfH'a.3 g ^ɨj<|eZ5(𚸫sO.Qm |\fR(,.JØB 4g*`g vINJ!1 &^"830~!'h3%aZkh@[,gis/VjI"+`'V(خ$M0[or;lc>mR#6F 7sFkVI 6"O<`C<A -yhcADƛK4l2Z:|i)L:Ner5j1c"9CN+%_Ϟ!aנ~^@P !1R䓫.4lK`ճ0^m,9`|[E}"?[k%[6g%6_)[؝lYi!:~w,GYR:VrҊ 1eBtt:ڡQ /zJ{ f=:˽Bu@EaS\p)(VytzvM [ӄwF, BΓRG8چJ=7O RRJGJCJ52za!^0_ a uԜ5\3Fv0:^r7`晜)@I!" 1{> 53еiiV[. x܃j+|?u L}4yM.kXD'7{A H]_ٛ,(RE%/lV`JJH-ɽHց>&{i.JN4Uc CpO7eՋ᰽ ЍX܊je5jċqi pD}WEHqID@sKɉ[n7;} r7$:.zRJNj]?EsRo;\0_a_bm@;[6h7—Q ~cy,)_oʱZ!Ebs$UΡ-j~M,ڍY_t [6jj1Z`+!A66lbCjK_tkRM%ϝ䚁Xuԉ HdAOfX$/mLB֕TÄ}*Qi'b( |蝵u?c;JfmYպ-lj而@IɢddVzIJ؋H~uodo_(z,'}LQ5Q׸ݩ$ QET]cyK.3G+njw6\ƆY(߹s)ЁGpT\e9R9՘Axؠe0_X}KmL_#V 84. AG \ -<0l~ ajqc6ЧlL0W˂ +Ef)7=<l̆~ Ӹ*c9avyO=H)uc?{}Op7%u\$.V3^~FZ3SuhK/З U;ri.gGGɚyQ{[ re <P6҈u1t+@!$[@{7h0l&ބ7*XMs ,!83Cc;7=ie&PT9u1HABR6Zd &=7($Mݘ8q*ˍ*1 WH䁬>@7'+]>#vоOwLmR3C [HʊG{pGb 3kgUc^0/Wʫ'v:z1` NEw->gD6t4*p{$em< @Qmbzj$b?-n?WᴐQr 0<;>boJ I_@^ :%"1ꋌn AUpHżs F(dȫ<[إ0ʘ^tQ\ PdJГ'w/. w'b *2u 4|kOua b}d_+[Tͅ@P[J*F U%k\N vSP[{_,$/>8ϸs$.WAXg roZ6C 4<` |SIOr xՀ#Ys>C:Iх b@7t[@wwOsrpF%N{xVlJ9FE=~@ wiHru,Ƃ=kWXDNCR, 6ݨsbi/+3z^ jN;_h~P*s,MR䙠5O9H}Rq}lɣ۝R[CnTI6J59Ζw#4c?χ.[ 8'|[}3*P:TqWvS)z(%zZ^!#,8YdCUJO*x9@Y)62&τ(25Qe#a6Yu ,eBy_ ٫aXun+wN5:Qԣ 8Q;-謨<}*JW,Ji֤a|@G'Bԣ! /pc=Y,jO'X %JֺbmZ{|fp1i!KȾnoOSp/J{ꉆ Q>jyS9Ch u|]{1Qե@i9# 5W=ibq)<&b5B +J%k8$zlKlpoLUCi<їsQEOg+Yn5/=` 4`챛`2ԴdǶpπp>Q.d[ C/ )M7bR-"9,VKV]RxS$2ig}CN>k蘷.eVyCX/`, T/Pfd>t~ ^88x\ oh m]|tu:)Z7LOI % 1Eob^ ~O9 $$GnĚź:n%k!UY,4LE$Ic!2/%P!CL0 -3Μ+wfD"HQɭ8rijkVHH(*ǟ`gm Afbw2(D0Dl_Lq/{l`ڡ$c[ IF1}A0ra2s _3hqm8Av~x]ns g'"9:Rl+$\'zc#!x}B$ecwK UÂqXe2:>zJN)jv7r}.>ނȾhڙp~ж%2$4JZJ51u>MCk4aNq/v/F;ON7qpDV9[pygﴍ$vUWޯ|W-}zTfwiC^]q ~9q k{֖u^-5?yFv@|:&/:ORʅDR!oe^&sC)@A<>>tY_p6ryR3-qӆ:8ə8yS5:ܡ~Uvpue\]Zj3_nW.Fq;87jޜ(c` ,\Q$cHLr+ZzG"I_d\fyC}7$LlU[ 7K测C*oMiPX'hHײ0TͤB\> b!-}h~N^6KNЉo !>H|ڥy|`ޤ)d^w αx˅J*Kic÷nZ J]+Qe ` %t0@[A2N_em3v?VP"ϋitW =)el0vQ# ëY}&mP#-QC3X0; %r{p_u]E{"chVD>kT#ߜ U g19h j6Ov)Z9g{Q܂MEZU{k:*#g`7=+Qm |ʔP>!JBBMh9#tkR2|6=R3EDP1;v?d{OiNX ;\yhQ6trEB"7KX_ӉR:2LYf[!)4jC_; M'iIȗ gAsz?-3UPM\zwH5T/]b"/x+1,&[LL, 9A(OcKv,=vJ{QX;=8ˉQWIa0BvU''FYr#kS ,xîq|r?;uIm؆揿"߰l^.@`S)qqI6-jjJ9E/(5c}K]¡Zy/ Vh~䗭g;SQ۩\PgKfVYtvgf̓ q|dF?h6ĶMn믁ӑhmʀ/iJ  L0cp a( }@R˱ Mqgv,-=17YK\v}YQ{Jgc2^˜7OU T䓙sbjں{aaY;l+z'-I6Rz4~U ٻr_z3@4EzU~*4")rXCkC(:E6ba9jZ>2)8I lU+GCf>9EhB <X'GSpaCpmjуq+Ŏ!XX~y%9/!4lQm'&zJ>å=0 l9#ʑV9u~l,T$+$ NMZRBw]sd<v&vӷ* M۝0JE8Mg[1οM1É*#%Ǩm4`S)Z!KT(S(bMiS&0oaęS:C0yVuPplLjgQ4!4FP6BĒarC0WfH"{5ӐL^o?%H^Ja~wKze%^{Zү^uvﰆAd@c3WCAQ=NNQɋU.I]?  $? qzUw-a(G ./<uqIȘevnQMݕ I .otESդf1:::z~}_WtT^Sn  -1VcI]r-vևPsuocuyvn Y#:CR(d(`i=kgEWfGkqY 6wו%h?Ivxl!4ߛK#leCS仸R }Δ8[#ǚsKD(;d P:xB`b՛KGO豈yG% `/uA/R.mE9@0TW#ݝwZmyx[+eTǠ m (90aŇ&--}"B|!%4>Y*êL&MGl0!*+Uyω fFD ^'aSjQz]g$4^Ar )霼ik]Ps} /_D6 #'Wx}0: wJ^p8lb55C޸]PFabH+:d-MXʺ %X_ ]#k]FƗM=+:q wtHq.A5'Wk[+edv42v3$~pmv*>Mzֱ]6&ܑ)Ų'~߳*ĮH4lsk6_lu&qTa1<z:RMa6+9a jrՄR md+nN0-1p1yX98lM~v0S0,F6s1XB>X8\5|z8YkAtbuQu@uyK"f{#1#v;\|eۗN`d`9G,g>dUQV]~"݌O;'%vb4"ᘓ8"6-!A6" 6KB>N5ӹrX90$@R˗. $4ym\6rDч̘-(< sW\SIVT~tx D:wmxI hӆY8W-Ap9`:Nub\t荐>u¸QZTuдb#|efRyxg+o 84O'Bk%:շUb[EO /wFe干';NThZT:|5b kt3qBǽDYjq-}E͵cR|wxG̍%nNPф~Փ/%MNǵxDF;fNcHغ0Ɗwy(5%#`,dbCyD2ȀΠSqFo՚~xUWկVQP>KU@] L t[9YIrR(BM1Skc^K;f;镠K, 07㘇Iսl [C&]dᜠ(n9[[`LX`ϢOGr$$/kOoP-^n{6ŕ^]%>#9Tn,zo } w!nf]T ;JXf*ptɲo$Y*2҅*㷼9DžzSGaR|D,)գ?"{]V8.72;N@#Iϼ[Hqm̏70jp |$&-fNU T~53TsyEϟv(]^;fH.*WLEM{ܒos <xc0V?MRX*ϬE}:9h^Ǥ(Mʧܗ =ZT E 0GxlPU cl]ԩ-M's*hײ2Uƺ@}¦dND8iG8'pX&&z8* 3*aB l&.T>nWlT%q SRPE"ZcQ³Ȁ8D=7>q8Jyc'ϊƳb..R 5j#)r+ $*cf#o,;@ MQr:WJe|Uq߭ޝYb\K ,1O/71ʻADGwKgmdI~kM]*r㗓':{E- Z.C`IfEb.`!+wXQGG.uD|[_*(O3`XW#_E7$/G`) EiĻw鄶6*x,7x@B 37ap¡}ŝ_plU ydQA`&Lfb`BrĜQuGїe v=FѦĜ2d;*T Ӻ?;- e9$_c'i1 2 eX\ pׂX\ KK>V)HUy["6Bփ6i=CL eΕEj Ga_zbĄ[Ѩl"*_2KcXG]rb jZd1E0Ÿ m_q;\,S%}_(#\hݍgм=BT)k[fȞɡ E4*Dqܲ EE 5BXͿs#- [g'  $ R)"$vḺ &XFɐ NQ* )kKjZEaS4tA}YSAx:OiTt>(v;MF 鿈5M6L>62Pӱ#J7P`b.dГu}it /GCW6sG^YwC?rfOk_JtE̱}cpm %$ 751B}?@7/'=sbka5&REŒ Z#?UC3=$mGg`]O6R XNpҍ3ENԕڳx l5Y|J{5~0+;Qj1O4vMp1" oP@ e"[dh8  SNϴp[rQ|k*Uni(0\W+SgX'fpָRAbw~s3r Hx)U«?#36~Z&񨰀/9Uw8b?u c\| c8M 12qKkc+7- ɯ9>MCe,C#ӊeO6Nz0'tbD/IJ؜Dsmw8Fz|.\j9Qd,tz W5/.yFn6x:uΘ{2Hr~CR*Koli1M 7vUD*~ ڕit~LTvޤѱTR6O4D֜|6Y `X*=\t4ݘ[}kbTߢHs'Gdnn(3()_ L w ϗWn)P?U;!Ou:pN:ǒzSc/&3+|E =>{nKpx$k~})wtCA@gHH9S 5 _ӣp u4oxϟݷ?D %Փ %2Hu IK_RZlI\\ch 5:>cIbS0m &ED(1c }ٌZݜWry3u[J"[L90 $tYz;vq<$"8Wy*40[ HdQ퀵gŽ*DM>?#&M]${u#nBS_wn$Ӥ~ WtSCfSD^\MvA}@N:$y"ΜYuP^)nc8ml-KI 2`"Bw^n=Gc%ū6/y g6m0KUrд'?`RG()EVXwqF2ub^l=ZUuǞ7fv\ iMJ6be)Фy'gڢpwPC~H):1ž_X2ѥbίcE~n^i5(lK=졍_6z7aɌ qZagy%U u8't(}'ٖV'%p$لBORK޵etݎ?sFqօW+?m\06p=H: J8S-q &W'xf9s"m YƲ.IM3,ڛzPD,TBf+ 4;zL[Q&ENz)n⽏>q˔"G&ӓjgꂉ _F\3Ft&K"&wQ,DdA pZ jc lo嵄qw뾢[ί,8,24FߺJ{O%L:!f>:TU -Q(svFNwiUV|nSoTaS07L[v"oԥ?)٠Dc)P|pc-##eL{N0.is] 1 عd :2kN51ʅ3J$0 Ld!q!2*|=;ʼn-Ҽ1@dIj-I9鴔=p%"HBZ-v@A+(m6u/QV`b5M#»xG7}w꽑6/>h^Hwu8SĢIYF-f^0]@Vi:#Nh{|ﰀca8欶94e%|%H(?2<4V&kdF*y̖p*Azg]ͷ|pe]DV/;Nԯ3 x;\NG%)V6az9Sĕ&}AlUƁ5B4"sO 0Dbje`f2 +N/ w] d,}+{$smG=7Oe(k?@Uq D]'u3&~i_1s{oryLfjlf;Lgt4/ .H2v,K|}mTӕ4ǁ_6tkU&Hhf'R` Vq8Ei~[d'uJv6 &4MaV?LԋmrϢ_6s J 5ް*-Fny)ib*/?i(N*Y-]~ⶍګ/p#&i%2"5_g4tuf:hz(y?ۓ%u0>WLۥ8Mq SЩoekA2 ^K~\+[|W@ͮM.$z "|F[̧I pSkgHPDFoTb xbq͋fDDȕn֢lլUc/̌6bHX#i18CUI^?pX۹R踴50d"_;v.^wŧF?v3Y}Fx:i7*Q9?Q\rhYb01]MS.pm*v1g^tw.=کC/rA _w%y~) %qBs; NBf73oN4γY41H̻پ>n )S[yN~36\ٚ.0==$͇ټ9C`d^+ %kZr4;E$ճ}BT!Eg,Z#$w ",9C]jo}-U휰BT0=v*#HS|VnP+IU5\lkS@ 4N;[-*[ʘJY򬵆تkSJe3Ao\awV2[6d1se´k)SX u}ö+4(jfab[#*~rCP9KO.ml0`f#Fʤ7f^αFCLQ2I'T{iM[TѦ;lI-zyas;ghes FjLBdb04M_-6̼{8t ˛c0R*|9`_^"Q*|]ƋXҵ gs`k⧕>0Nl98]RytFBzQTb3RǶ8"JE: }F!e'5G/:2-LG/3LƸI΋^ j? ^ZX>-BM#/OwGj^RsM;DcKN8+q~vUo⣼ɻ\,.00эF-\*'K3-Xx7lkX2Ͻf k+7]k8 ./C zg"hj7rcOLt=q;Dq5Jq8CV^N/vսtbD}~Ji`=i| R<ê<>D{2cuO?tedud85~1✣ #P:*g u'XBg KHD7fsDV?sfK.m wѣd2_܎Pzaw:#}DFu3>H:x[73=?zd8voxfz7L鹁@BkXs;=Dk_G55Zɠ+9LZ'h ϓz(4U gwqC/ jwgxJoex Zr6ŭЪ 2/' M)Ǣ:FCpEhE}n @hDGg9%G3UhB@蛖ы[) &w>"@w2f&E>* 5 'I5Д.ʍ[PFP]*b5sc[< Ʈ1^{)L'(?㴥z@_jJݫE+gټsgv,- l N^gSYC;kq&yY[ Itƛ)a0R319"(ZG#򞝎)Qvnq]黶E1*5ɵ?quUݦ-zxc7h9mm۶UùWע|H t=f єan̦46TOuwxF#Bv"yXb\|VOrߦܚ}R$tJ%)le$GZ:U$ן]lTn4$'Q".% .v+Yh;J`VH/*/gU~ʳQU& ue!}z&f e;3<,ZY 󯉗g,x=u"uG]eaM/'6k&b0-ݐ6<:;+w)8 PJ vr I@-/4Dpj@'VQ5umPJ~k\r ס8"^Ƣi(_h'J$.o ~pf AkaEf,%=k3(n?۽MBU2H+Q)]3/̻zf.!yMXj_]l,-g9#(|\)\>fW)4솥Ó2D$*h⩿eyO劏LEjAQy&0L+-`V|zW[[V.0Mך36GO+eٗ"ROZOceϻr2XS'XK5~6gQop[ﻋnOȫ "{4oN3J9  ϲ}O5 8=bM)sa?_V2~)0,;A|^E #&5^M1a4{Z)#}pNIXeAä84Ntor[) >?ġfn}z@d>w5LdC{p2@83A&ƨX}<͹2~2yQ[7/ /H(#8o IÅ߉vAkS~_&4DEI_ϗ%_4{ɑz|cuJT`"jǟgx@)cy J2?z W`p_zvo,3GRs.lN^_D\mfA~[5v"m>xZ,ݙm}DyUfڶ٦]I_+s{H<-^qώ ӳ !<@߇JGO)!Hz0BV1R_o}XR5JS`KC"D>^Y2x'ۢ3 ̒1VxE6}3]P!vR3Hx!OƲ 5uiܫN yqt 8ڣru <"|ҰD#.Rlq2K;|(3m92BM*8^ YBi`.h;SM) DK1dFˀ1lO$HV%ҽ  x҇mֈO 3H1՟-M!Z/1Nr[6hJ@ cl]Ė|s k4y20qQDPKSl-L4jY@3|yT6^G)^c￞UI)^y,d;om4B(F V5~^)W2n8TPaR#­Pm nadm'WR6:qM7I?]~߬vmEΊ'x)V"g#چWew@DM\i#n; ɡ"\H:4lH}H='tN NěEU6D$ϧ1HPyi- ZXVuލ;oB$wQ>4y/[@xB'0XTCʨJR~ҵ(z۫2a5^4%886>Q͋$~qM*9e*OQ}Һ;H6űw9M]p‚{{;m2l,[M)tݿ1X ^آ,̦`Nѭ91EA\e;>9Ht;KɊP 5Oq#אjƐUs"nB(pX19ޏ.wxkt/NnL5_y$u|Q# 5W_κEߚV2q\Z]/ h{H-ԬX4-]=GYAl~EGddS}w'4t"R>I]w 1c;  Qn2veY3nNd5؁hc߳J‰I}i/K0 Gki3%; #:!3{S0F ^$,FfkdAyeq{ ϶ڸ[r.8-4= ]kR^[ǹ[?l,&덏:)Eb >[9 WIzIE#yJEj"BiȵrRZ؆p-h 3PcJ{QN[={F 91BA$=S -6jšEs]"l0ox ,/qcBeO Ak wT"LZ= 1_[_x/!H}Y㝟U+Ǩ,[~ otI /!$Nxc39(P@ƞ%>SFo1Yx{Pd4f+8+7ͣyބS+o#^T)%EF@dXRG9/SW1S6E0%I(soBwoq}RwsAŒ_KJ3Ցkqj=׿iH?-,a쑣9]q+ϯ{ˤϹ By}d9WJp%mr+)vǰ(&2OQV`mvXbaPǙ+E.蠭n! 啫ͬ[# K XʾĖ[+~=.YWovԭaɌۜgԄ"֤{r)8y&ܭ*UK1N6 &25&žo9<וz2i gOVK@(s^=Ų^:4LjLM/>ڌiǦ~Xh(Qlq7p9٩b=͘imM䕽AN~5c%5e@bwF5qCgW,F;Ɣ,וNʽ'\4 #E~yn^ șOPQˏSB Xt j,.Vkb=[y}h}J_6[dj1s8?D;M l O^O1uXb ,oEsN. N <^5R cLj,im0UAO]@dzX|"fγc]N~7 >ED1ĽX6|Xo=pNY~c3%xB^I&@ShQ'NcrG'{VǿK`uo'o:RQo|i3'ѲwN‡1L뇞COoVp/Un /R+ _Ti˕% ht6ѐa&02/o轭`6ҭGWz$]~CBƕ鑜TaeT?LCJK`HynY6ƨyzٓa kcTv)a)kM ڸXDAmaȮ:~?Aqf';_ ںh.7i+ݲ[_uSi>N8QI8j`x@Ak 픍13ge 湿)IU ?!$,j)4Ae8ܿ[}`a Nl/>p?CʅTd_[ <{ӟjA_"ɡcm%vn+<ѽ(Ѥ{J4WYo.eZ27_7fy%X՞UeQhW*=5cuO_.W&ac.z̭u^BTg*2ZX^'E #bEy;=T`bFalx|f!H·.C?b*&jܫmclA"H[! <5^2ťmɖ+l,Jb3(t/:)QMz%(Ni_]i ., ~4|~)h Ơ|w& {h*߸n+:@ʿȀ$K{MWwXY)6@y?ɪ;Jxe\wzU>?zFb6wXǒ-+fI2Va}ǹY-\[T=΄%%R` Dx7DݵZRĢwLgjzIK@9"X5Qr@$-?^ -n:^qCaՙV#SpO1͏!j .tKkᄫ< RnZNL&adO72JJ]g祅',|{ er{=e4*mJCXҼ1; 8P1l* ,ݣmGbO_d4cN=|  e~䬫p&BAOdm61\Ď}L 'RhϓSB$8jul( +[*p 29ә :$ i$MxgՔ?_PnĈV¢9m\ņdm\c&zjw1lG'Sܚ= >& "h$?m-drl/@z5 ]zFڱP+pzAo.>bȡ/;J%EfM,9xE꤂߳\Xl?-B6Q1uFad7hc ; hΏ'@PWk6m_$]@߻s2ݞq__ϟEAv6"aU&5*͊R HB\!]a7veb[Bp.A-4Ue#M c@ӐzHfmpuf)vKtdB᮳Z{Rdփه֫\ 䲊"ƛڍ7IV;#9X>/JXomUW Ǧޒ5t P1X rh?.1h(]s 2-#֕`4rA+gҍQ{yq_$}ͯ;BgptE&]ߞM'wCq\W3āq떯Xf&~id%:P򅿊E N?U[ȏ3LCiw~⣑Sg緛O։RZ m?@.*n&e 5~y k}-lix+88?#n)ODalz'ۓ+sm:i4,iyY&oE=q.vϠNU}Z4~1h;{^))6|]J$wZ)PogKf'd N8ԩ/+u@=qJqDea:DoJi}+Dk?\\K&ElJ[Yn9u$]Kt`V~wɁlAg}!AC'7E %R`|L=eVZȊSWY&[grX *ia7Y.|{$m| gDJhhbl{>͇֗:ƻ\ZS]L&ҫ?̢S!WvS~eHREWgwD(kD20Uʤ-*Bdb.CDŇ_ܘ:`ŴXRs^2Ffm4x@yUrbfؙfpĖY=WebP&69[Ū2m-$U1{Z`Ĺ ;cN]~87Zȩ\@ \^n,@skzN K tC:w` g ddZbĴGM"ĸkg%I y{؆*+F0AҥE\-U7JWg4-dAr &O) :DM C@/H).|6cRoKڒsҳ4SDp˯\&+ |vh!AĥVzBMn51]%qb8Vv3Y+RaI>gޔE VKR P~YN~^\uL]NWnv?aKFdM"qpyj7\{w/A6 ?9#CO[A.&+ }R*E׺O/V?+r!I"|.Xkv-P} kZT ن*Ay~z+2;R-"GDao(,  ΉHA~ [auC=wD Q1wHq[sȂ~O) :ʷ `k7`_LgLJHEFa;6D;nڔ?{P2ݾcf`;Hei p9bcG6{jI;&RFᗼHmR޲c^ W9e'0ZY5uV~"eƈ?CC=y%wI&]}c`Y.a"ص bJ|׀_5ԥ?7s3'$*̇מ?p[vϧ28~i, b$[p>M.dd@oS69?M܂[^s`٘uHynnFB:e <ϜԘ'F;Y+$<~73yu!=Qu:vMh O0O9Ow? y(= 9h!M'V]ν.A7/3_9l{S\cUA|׺MrHpI+JZ5VbU*Tq\<e@Z9vDpoSQ?|`v=pN6k&ݰʜD4ѹkc4I9BNwNXUQ?βT&uG:7g1lX>!i*bͶʁ̀y!/d _۳6&h~erH^YNwaFfȚ)P]- ~KKVn 8M,?* CT{6.!v•]JmWCR_MegZ@H."Yv-&ق9.&5hyC9'KwтRs^#bV' @II"6aG kv ߱<*= nVѻ!\mT&VQ|{ÿyNs<%!&_+])>FlrJV~P\3AN`v=Լ&79ڃf*Cl(5 tp zʓِf\ p3%ݝ -;`Kb2 ӗ}ɮX' A6)[ծ[ %6͌[Qᴱ _gڴ%ksʍ6% 70dc]M.LheZ!y5߮Ikyķ=H-bqG;*mуh M=g7_kޏV(VS7Ҭ1<3glhAFOqd uWa ZsӔ:h\5.2OM?QѶ-zQD"3{PFA^_$oHdR؃:(,d_YnB3&Fd#Bfba@K4 _7l\T _=㩝,tQ6ii4`EVA:pܴB?`qٟ@ f! OhgI3B/r3'n*UrvZ㑒s.@eAoWmfRuXi/SeI p-Ӡ! OvdGe4ce`N,_m'~{*[Z}.Hx$h'uN؛G7"~Tp4}55wE76X>k"D45K׳ż&p~hSxP05IQhTm*pCAF{zZȬXrsnҍeDP>RuJc \ ԆUW0eoѶ(]X1Cr*8 R*9[Iyj7iGޫ8&tΒZQ ۉ7%9A0"Ι666Ͽ~<X*3UY l6h1<_I( 5ί"J5)U _vN\ouE'"mC}uۏ`gsw4%!ؒ w@̟ 9BuZukʸ/ FM%rLcbFXOAU#/PXm@ *~ kmƂT k 6L]'2 *%…d jiKuQpX {q@V/|W]7 n*{knfXWIXR-;3xa~j$v1,>šϙwzܿ>5➿+,rћ%>$'M/XSX) v٠W89o/*Q 7a\)t~6~<5*,Lx͞ecoL1}0>H ;r}m}pFhz1*aS{ ;w;Z<*F?ߔ#D~FQuD &xۮ'7wJ {#Q]M8z UNϱ0dhx\|E8Eb.7HohN/joiuh$ 1jhcE0˭Æ =tM:߽Z)NT)X8bk@CO 'Lu>4x LvU<җ9ݘ* a6a4gvfKNp!QެF ypj9uh1 < G1*ydbU]")!xoX}JM] ,:BE^D{rDCS5J8]=X*v̏ TA'4* ۏ`)"X&Qz` ZPg)[pɴ߁&ch8n^:bDa,z*XoH(WP2Y1 dj|yiPSJ4b-C+$M(A:v t#b6vK.*iŽӿiBo211ye=NSZ Q߿̞|mRG]6y4iשb#JE=;9O4Dz䂝r ҔsM麸 }2//Wm.ZD 6s:QA`?7 ⳏI+U6}@,,O u;60">Ɇvp=[M(#P~vBFn2-Qfb9gʏl~Ng>6?ZkWH<ジuCHsFM z7<*B[]BoEK܉5nONĤ<`ԵzÀ9`BUJ(68GB_E};aPިlƕ*kHBԸԫu 6|5SHGJQ"W{ul.%`$2 xϝd0¹t5wI,$=qLGi+;^-ụx3t-%îfA]G`'npo Pg.9k^_껥4Ȝt"c4Q-yqOΨтor`X;hZ< 1bQapI A!\Tyz]0HϢi߉GC[}5+ڣFMiLo:Ws%>=N2w_ۦWjUs#+6;(cvP?#4}|LDZn2\'-;)Oy~i{mk~!5S-Eq;11R\!zUz*iƃOtmvhGu/ ̊K* 8U S:$'_zKjS# Y/not1UmlW5{_ճ!A(X.F6 zP@Y}ـ^mmm["U6JwkWO3_6 }#Frd`Ѩ>3s/w=PVI-fʒ>$RB32O74\?*/J#>~kXTeE~' F!]; }%T>.3B} 4%䛺g4N.: TqE%pi9926#m4d*~,ɠMWuhbkOU,EaäbE#w0*uDԇ/,-k]FDNfW%uh%_}0kx;@O"8憇D=#@ x+R[xA7u0@.=901.\Q,ZiLG7qЏR/(q.yJJ܈7Ub}K`6A7٢Y#דsInﺪyz?Vo̚V4Ri9xCw,yHt@(l-|ިԀbKBwB}Q~6z(GsG7=4m;'OİUw>v.X|#wxNyfI];= I+DR e=MH5ē0B B C*SGƶ̩ȁ;HۥpW!* ˵Ρw94[6:`q0lmOf(;kqn:(;a,#ȭ=̧DNF|`$[ɲ,q~I~ԧwJ^YR@[/MHD!EsNhkh`5f6=A7غT$LQd}GF 9W rV;B(   De} q*k63߯Cac;A4~7M }*1Y]T$HAAS7KChW%UN@z$Kދ߼@3^/ANSE 8 GE֥Q*+SdK[:;Ïho1gʨkkJ`ݲp F+& ddjJ{Ĺj@`UZW=uv qc̘E? ݻ_E@oN8 j`V tt4Ň`vېx\6R鰿D#q 8N_e-hEn@t|dS`'JKۖR03UbB3! 1$ ("R[~LZRy>]4!gpw[_0Di"bAjUѦf4`'tmcKX>1-\\ۯT("7K/Zـi@qH`9w˲[ɂp,)o JeSX'zfem*3H&)pQX.ۑ!iE,՘ԄYB4$~:/+elG}| -H mÕ }RlpJ=(Haq6/C'),c[PBlO(j?:fÁqFR53Fbͱ  ;{8@-NY㨙0o Ak٦עk.$ {u™x*dP7C$ҸKO ͦ tDUݓ( Fh 0UZnYbo%nh5d:qiwJhB㷻&V?nd$Jڵt{𿂫݀ _8[}͈@27uՑoRPP;J`l療N7n—c}Wynl4/2e=l~a9NepK!OFa !vʯӢRe] Wwc"QrY{QC{T?^J:.e$o"RmtfREh!0!nU.cM'4Mȉ^m3)VjDQ7rHeZIekDe#s14+TP 1gK{܅Bjfl*Te VIXMO$?5r= &U-Wb\rWos:fL3$vM(:BuDN#a@VGogK"p?E?q_+rWZ(vtcg6D, Zt|Bwt^6aHTG ;ŏEH|ul̓_$:ٝ0|q/R [m삱|)0aXǞAEl/dnȵOHEZI`Pv`Xc loY+Dw#$=c']"6{-0|5Y{X1SkݑYݵ_dSCPr^"`Su_Bz- >+eڴC&iՌH@M ֿN-T-tk _~ɶFB*bk_Ui8Sl8À61F؝ʒiV߼4|D/ޛx*pƛ@.`6+lGB-KYN;|p(3f>lӨ0()eO}F\VAnj4djDxC2PAjumbjumdc2pa8qc2pa9jumbGjumdc2ma8qurn:uuid:85d00832-fd1f-4d84-bd31-163bbaedcfa3jumb)jumdc2as8qc2pa.assertionsjumb&jumdcbor8qc2pa.actionscborgactionsfactionlc2pa.createdmsoftwareAgentgDALL·EqdigitalSourceTypexFhttp://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMediafactionnc2pa.convertedjumb(jumdcbor8qc2pa.hash.data~cborjexclusionsestart׆flength: dnamenjumbf manifestcalgfsha256dhashX /Z"͛{y! uKyC)FcpadHjumb$jumdc2cl8qc2pa.claimcborhdc:titlejimage.webpidc:formatdwebpjinstanceIDx,xmp:iid:da702dc6-f9d7-4429-a0a9-3f586c36f18boclaim_generatorxOpenAI-API c2pa-rs/0.31.3tclaim_generator_infoisignaturexself#jumbf=c2pa.signaturejassertionscurlx'self#jumbf=c2pa.assertions/c2pa.actionsdhashX *$_دn@A=^curlx)self#jumbf=c2pa.assertions/c2pa.hash.datadhashX ;Z(lk j=e]calgfsha2566jumb(jumdc2cs8qc2pa.signature5cbor҄Y&!Y-0)0"C`Z_|ɪ׀1@xv0  *H  0J10U WebClaimSigningCA1 0 U Lens10U Truepic1 0 UUS0 240130153453Z 250129153452Z0V1 0 UUS10 U OpenAI10U DALL·E1$0"U Truepic Lens CLI in DALL·E0Y0*H=*H=BS:WT08;Ea18v0C4_xۄ3æuYgz(pb1h%Br֊00 U00U#0ZkfӔA} {]sKK0M+A0?0=+01http://va.truepic.com/ejbca/publicweb/status/ocsp0U% 0 +0U 8đJYX0U0  *H  "**64Tj3X1h|oԙY:V\F &).bY…S8lAUշ1k͒ q 0rWffWE5I^"DSvTCUfؘE:0-56&Γ,*“r^a:[&1mUVt.#^݃u`Ц2|VK=Fod9m2c4bZh xoMnhx#xY~0z0bỉP:_҂(0  *H  0?10 U RootCA1 0 U Lens10U Truepic1 0 UUS0 211209203946Z 261208203945Z0J10U WebClaimSigningCA1 0 U Lens10U Truepic1 0 UUS0"0  *H 0 çPkjr3eA`(k "ŧ (b; yeyXɭBa]CPoAl%] i*+68k?~mPw&G8JK="?Ro;^9t.o#%3`s߽> 9.wNBļ-A"TL\U݄@hi䲻JKu]s,Dfa'qP#!Q6Vx UtS30IL#7<>IoLlH{Qߌ|i21&! Zp6)om )xUa*{P\Db݂'ѢfsigTstitstTokenscvalY@0<003 *H $0 10  `He0 *H  sq0o `Hl010  `He a]0ɾ$W:WOVbl:kj>,Z520240701150349Z cV[Q` 00D9?_a0  *H  0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA0 230714000000Z 341013235959Z0H1 0 UUS10U DigiCert, Inc.1 0UDigiCert Timestamp 20230"0  *H 0 SE[>T#ϟ] /Hz;*gbXͪj)bciX5q:P ǚ;/fii[+ P0hʃB $j;]E alq^<.yfR>_CӄH-^EuuRGx)9kxYD+JՕdM#ʆ!dpc.$_v}1eGUJ$/+{s>2R4ԻԠ,4nd7QͪLfhbAxmXAر,Qbi|dM^Pɳʼ;hD;Bs} y4~\ XL>iuǃdu͏vV$k!4/:k*{R8 qlq>oaG l$Bʠq=ip' O6_p .d"+(!IQ~f;8QʔP:ӊ@{00U0 U00U% 0 +0 U 00g 0  `Hl0U#0mM/s)v/uj o0UdVe1I0ZUS0Q0OMKIhttp://crl3.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crl0+00$+0http://ocsp.digicert.com0X+0Lhttp://cacerts.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crt0  *H  ޠpO_B֏ѪUㆿ',AК3J6Թr~y8H_=2u6gZO5<*lyD:8;^9X|s1U ~yeh";뚂5W(i2:Fkwlls:IF̶8C,NL}hpw \`(8RZ֬"#NPkwqDAɸFl2|X/gGesk,FA_٭DA0067$T|G(f*^[0  *H  0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40 220323000000Z 370322235959Z0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA0"0  *H 0 Ɔ5I=rIQU%7Q҃ўLm̃ZDB_h} 3P &smW}Cs+"=+>BgQ=V(-ӱue)iِF{DA|jWz7y]dRvGa_T !hn7!@_J}9gcl6 \dt@rźNXMy׏s,9H1W)'.NvU&p&G CCc{un'%:8;["ق*ǒ>sZlR+Xt@(sCJk8)ʪsBhF:^KvQɌ ;["&}_#dc>t? v]Fu`X (T]^0Fvk 3ͱ]0Y0U00UmM/s)v/uj o0U#0q]dL.g?纘O0U0U% 0 +0w+k0i0$+0http://ocsp.digicert.com0A+05http://cacerts.digicert.com/DigiCertTrustedRootG4.crt0CU<0:08642http://crl3.digicert.com/DigiCertTrustedRootG4.crl0 U 00g 0  `Hl0  *H  }YoD"~f!B.M0SοP]K)p )ii>` \[m %41gͶoPLb Vs"%Εi?GwrtO,zC_`Of,d&l|p |屮uOZ](TՊqver#'D'$&*yV Ečrjq Ķ͇$OIwfrKR7~S;I9z%c',=?kfAO@!!@з$x:䞭4q&k8sO?;xLĕ{ _39Axz8#(_+~Fu,',&o{6Yp7 O'`gfU:)+A:1b  Wټ2]# v&evB) G+UT++/DJ78+|00u-P@Z0  *H  0e1 0 UUS10U  DigiCert Inc10U www.digicert.com1$0"UDigiCert Assured ID Root CA0 220801000000Z 311109235959Z0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40"0  *H 0 sh޻]J<0"0i3§%.!=Y)=Xvͮ{ 08VƗmy_pUA2s*n|!LԼu]xf:1D3@ZI橠gݤ'O9X$\Fdivv=Y]BvizHftKc:=E%D+~am3K}Ï!Ռp,A`cDvb~d3щίCw !T)%lRQGt&Auz_?ɼA[P1r" |Lu?c!_ QkoOE_ ~ &i/-٩:060U00Uq]dL.g?纘O0U#0E뢯˂1-Q!m0U0y+m0k0$+0http://ocsp.digicert.com0C+07http://cacerts.digicert.com/DigiCertAssuredIDRootCA.crt0EU>0<0:864http://crl3.digicert.com/DigiCertAssuredIDRootCA.crl0U  00U 0  *H  pC\U8_t=W,^"iT"wmJz/-8r$RN*-V0z^CDC!rH˝Ow'DY/ 4<LJL@5FjiTV=wZ\ToP=v ho 5` X@cŘ"YUk'lvo#-~qj#k"T-'~:𶇖[\MsW^(⹔1v0r0w0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CAD9?_a0  `He0 *H  1  *H  0 *H  1 240701150349Z0+ *H   1000f+2]ΪO@0/ *H  1" -&Gyp1 \e/uŞrF@07 *H  /1(0&0$0" mt"@WhA6oU3M x(0  *H |% >{<Kɡ8GwrK]0 뙘$p^JY=74mr*Sj~/'Ik&`al<{|9䓳 -mH-ai߅5,kAd'3 A gė?~,!-q#,?>f;xQn#Qe߳khϑ,>Ao`w0TMKO/-嶻Uk$( t:LJ=j̲ Jsq;'P䖀sOQ!֘[[u:AkL jYKa5+P%H++InU2O`+\A,|=rR7Q@pڔF-> )g'8 .e{./oYMw2.Xvqm}֗70I#n}( )Pw7 ( zcM%-PEs@ E0( nK!p{, Vož8F֖֒$qG# \rډRr5RC'z^o$XL^GWRNu-ϋ-y9&0Xrzwh򍻲xwXTEG@U g8N0Vw6|]k}k].[+Jz,(@ ӵ8FĜqHOaQ`/z>c["&YXp9*xNhEY1brĜ8}3ަ@q fSúD޷a[&!V#'un>F|+ڇ-lIe1R˷5uF9oDp'&qu ?xOG`>3x"`|yAa^;?"e` O3D@ePW<[3s"ė f8pW=1Q*]~=UWY2+ #ߎ梜]"h< bžck+u޸~]+)SsDA +@,I8aܫ$F$JfiZuߋJ%E&'~Jy:ߋ}Xg,nȆK d@8{Q4rqacOE6a;yh]`zqr|} n@]]f!V<0xt5~<|/yI5ydc+?@:skƾ{x(rȌʃؓ֝EڤchS@ @ 1@ ((CS@hEP3\}WO8$GQYʢ=_xП{f5O@ax ^jj֗)lI) ^ ӍU'`<6kR@C\p8{G$x7J[1%6l;y#{ԸF_i0%pK_S=_/e{YS*xF>+iݔräd j5iMBp57_-ݤFXS-""!7wƚ\JE&h=(Yi'}CXȱFi$Ϸ`8PO\vHEm=xtȨެiAv3ȮyLH<Ǒ…S)(=hZrDfOmf{ 6BjgJRwGx<5'Y{OoH~{K+c xCVysE}+pQI69?hrRO|s۷VҌ'Ha@\((y2OA@\3feլQeP͊"_)r@3x%?/.cUHIM1]ZB-X h4(PHi 42O4'>+񥎟xشp:/;GׁN\O<,kRƈ,@2+se'o;x=_[~5e'kOP檥.Et+4 5 4~Ѽ]MN_\yczv&jtIǁ1Suf2'-306_]?k>W^L&| #*qoT{i #˅.iy y+ooG~{JF;,//B'{qAীA]CS •'[}D9~ x U?i>qL?_ O o^=c|!o.^'&Qf#~7xg>O:nBq>Iw?CZҜy[=ݗhDސVSr+&{ OjR52./9Ƣ~ي]# qqW;X៊Ejז}u Y'nM?ּE${R%@!~Z` ysjlT}烴>R[q7sWJ[)ULvIQ9;Fi4*M gR>}UZ}o:%I su9sj3Ĺqtu? y0剭#W:`̜5DVsZ&_~@ccP un%Px\KSU'W#?zh'?a;\U/k0 W}fjE)_7 > x =2O`qʿQ-)~鑴vv5f,@1u̮C9b@€ր(hZHc1 B(Ts@;ω6y;Y |{xD[ 3Qx';cFj:2|IxYmxgVy_FsWVjI Y?$_[b y*Y gԊΌ[(@21*q]dz\?f Qyd+ofVC'S֧mڌ蒹?vy$ݧTFYݤ~tcW( R]NY /iY"70 {JE[ ԥTv~SP,~i!')go{M0OM2pM*}Ӱ73Od.&ٍ;!^ѯOGiws̒XF5-+xDEFxQI$?a9*Ǘ/%Fr{q+hCG9vw 8#.ߐvZ[S?GYE8ST|~3i_M/9LG෍вܨ$+8_H4ko\?xZ%-Pn|?$˹92}zGGҼUE6h62ݍ,qsѲA|Zgn-V8HO4N\qZ)E\mY#֑1aT>b4{k֡ RmGXS;V%%)pC@/tu_*ޣ ]MmhQ#eZϝMiSIXΣsIES]VdPLs4M]/֙ ]CȏhR5.*ȩX݄&)B#SPh3Hu?JԑP>֓7t&48-1;ךw>>;I=QaMk 1㷎{j&w6o8Z0X躗-mD-JFp1~ޜK@F]'HYh-4>eżX g?4rIlq~ ~evcAkG%IҼ1w8u CªG'|c+)+Y\44'=x:}LR$sXk +tyT *ORGQ^kE5-cҬGj^^%x7g7t'QӤ-EѺ'}E*Ssjğ_OiWhG/$V:_-vdY9uV>T|v~|;|k}~Wy&+1C9@8;OJrqzwwwz|y᫔RB^,˽qOgSM$S[i="z皧Qrڋ⟅v-l9~PG@_ %@ISSw,j;x|ѥ#YF''e '4/9Yͪ<߼ պjaQп\x3o÷y5U1)$N0=]/_}&ć!lc>zS sW)n!סY'/(XAROPzTvz #3&k= [ەc }HW$ 隖y-e#G"0A#RiI=ElMoEK#{#\׃Ia'(Gbƣ7,7sޱrm"k 1~ˣF/n~S~󦣥ވKWdr]zq>;g%*1` NO"jag얏޹]yuu<<.B$ڼy۫V0l=$iR-R2< |MOw%X SWF?3F9nU eRR^ N 5[\yqf8<8>[3 om?PG[f0T 8 =1{`I?;jPlj[>|'-#6mҋOO*GezW5?g]GĶzqJ3t؂!I:~5mZď'/RL0Q c8A ʛs┣$<#37v7OqstgH&d7aֈL"1h1I?E?5c{68LO,y?-NOds~>Ave `RFFOQҹSwCZeKEǧ٠܊z>5ZyцTO9]#h<>c]+f|S}ƍ _῱A&_szuGL_\sE AL6e6z=qZz,[=㏇?4o#rYjwP{X *OR.sX^9ݰ[98\Lq7}k<5Ck.hCtaazQOyJ0[nU| F2bŻDy0#,cO1s9!-N,;i4$!cy\4nO֮6'(>ȟ:·kH[DY{ӿzrv<kچu PM{'ŝ ϭuebKŚv$nNeUT,N;7υ^'./GFT.ZΝNv65#$mL\4 uPFh@?s}\Q A]N'X}RE$cGL2hl-s;S݊M:ZA ?_-Ϋ5ʗO,%V`@#׬W3ױaf[UbOśQxZCksGqkNbS xXcϟ ).L ^yx~g'"ěXx^n۱W'񮜺pybo̪^OR<$|c;kiԵE=8L˖ vof-|9o iP#ɰ\I= IkxǗ\ ЮxN@={ֳ6qw}+Şҭ9nA)7%ϊ:rX[rt ?7t&¹ٗtzޱPǘ29Gd˞S?{hojZ?4D,h8~7%M[BDͽ!Z׍!9'?NO[4iW%#@ru :SGݣ9 h;դx$T]Xgin9(HߊTwgRΣ,֢ۢ4<%H8HB#ަIVďI̭ũ<ĮW.s (N]˜I5xj,}?Z l~5_FfmvS?K/}2"T-D=pT8#<$|4}JRG֢_f)CH3n++⪺ʬul-%AЧ45sf[k h.nhP),}kE99EY>B[]LZ.'&maQBEtb>{X5ojݛֹ^D{=by9] {;/'/.Ү&T[DΜ-Trdc"soPm/!!xwUT]KLT9Q4e_iz7Cs87zo|9⛘#Fnl—qcmGxWmò'01 +%̏ *[0,Вs Vhž:L57JSz#;Q~̵΍p*dž>;TQ䖓_e8UJZ2Df_6!K"Ǩk¹qJ5=_¾OTd'X uY3!(OucNki50i%SE?Zh N`鶩&+|)]WgREK˽CW͹ݚ5v iguT# $=Dȩ9xVn|M&[3fCuV,0TN7dVtM܏ݎW)X|mr{~BQ1m=Om$<^!уD".أjF }XcS݂ [sSQiS񍑭 +/~3ZSmggx)RY-ùX~k9gR`NJ}GRUKt?u7ep=#&khw,r-9L@ǩWDTp˥xF.L9V[1Dr᧩) I^Yk쌦398um?i%塍OksksjϣjdF" 1!O02+XxNUuq޺|RE9u=J $#`}P._qqV9ef+i귞 4xf~bX?]˞+]߲=HbW%nHbF;TgI:\ua5|oúr@q~>˰x?{.gظ1vl?{>[q4eU =z~1ũ((Gg唥{zXN6J2Iaܞַ*# 7&5s/(Eʗh)fz){VUaO~.1q<*e*qq@|ӴbOސ8K3մ'R'X6fc;}rkRՖ&WO [jZ+JNI_ҭ=nNi ʂܜnLUSr_okϿ_wN[%Ϋ=bW 8jS'{ppi)~Z0?(WMQi.c>5`tr"k͟x_NĞ2rw1yF<8P3۵zV|?v4/YG~kE[|먑 (s@BZii im%wX?Ik?Z.#.;;'⟈购|(Lm3Φ<a@cӿaNOãMR|\d6g*Е(;6tFhԖ.m~IWVGmVek|+[:ckzy6"V7SISlI0 MCۺ0\+Y^;FJ9O{ ޷I9YfY]7" zmr:MC#Em,S#Q5;$R ^V:? [.>0㒤HJÏʽi#g».OmG}FQ8] ь+,pB7`'7Sy/Dr,E(s4#̺a[b* t-(r#^&~oEiΝ# ~FKwcbphL {D gK0V(XRcxW2ZNܬdGgl8W5(s+qkCx~/)W@LW4_(ڔ?$=ՓS8ʘ7X]Y֦3Olxlt7"oOBgE{G<Υb # ?΢OEEiɶջ@4簺$?Ug? <} h6_hB9&衚#Jtں`|1 o,[.S0~ň\mRWGMM:ej9k9j}d}-JO׵?W?+g)izƧoaBfp9'bO\xb)h kAh/!}g<3T]ėVtx#Pcf>qim[Z*DvdAܓVϗ?OG,`JdfùJ4O?ޗDMU0zQ"_m硻;P0@bhchDGz~ˈܸAJ?购(Lexh[[zɢ`r+clm 2\1̡^^%ݖLc%=ۙ^MzҾ61]gN+_*iZjz-7 O^(TJ<4>*WVաeh>Q瞕%F'8+Qփ(/|ɼ7c<68X%GQuN-j#×חnEij8`f\QORyOG4z͔bX0mqݒpzzQN;TvOC JJWkSoY܋ox~U}Teny{P]7ɈGiWbΙ&9v"qR2?*QR"_mgk@oO-FÂǟ zY,F#:qnx gY1¾z#9Bzz ]Aj;=Г~ e:եS]Jd\{ kUa8RJm`B{ +CCόf(Kwohlw}Jc+QQ#I#2X:&Ǥswm[(7<׬ 5vix5>"ַ[%7gNjy'SvC˸JOX^``" 2{kd s.f%⾟ GV}x#ԛ |+..S#ΐ NɬQ<\=VWh=[rJ5+)5/f90y毶zv }s2uYnE4e~V{OeMoG,{ [gsb"|wxW]ܮZiLz9?DUau=n D+wmG~ǮsXBl1g?W-['V?*3,g/4DV1>hO|ceP^o1P)!C7$b]-}HE`H3F9rN5]]/z3h%jY"ۻvx'w gӵ|'4UM{eʝ겞GעO^E_xG¿6z [h uK)p~?j&)ࣻekh;g'w+&"<߈+8c<00Ns9񦩺Mi-~m~]|IȨ_> =C8wNZQ wasaȢb򵌨։[M2%k(9FD־ &]OūVg?<_ Xp'B䁌@rGOJ!7-ff|7ڤ5{a桻,Q#hP)LB(l C@?e-k"xƃ>!?G8/ ZgąF$b0{GDַe|O}36ڕ1#!hQHKfZ%(w5,"e 1R3xiC==W=6WWlDע:;9Om/+翰y>O%{,[O6qu&夳!l*/{7+ؗʩި6ݩskxZ] 4QaxHJ8B7_K<"Ecg7^d0[J#t.qd~u\L!YQ{aK :%]muZ]?L4-br"<ˆ9ϩPq^ULWʆ#XKk=7S FI3𶻥:ͬS@CE"!+a`(Ge^+tOZd$}r?Zֆtj1Pfp~&fBklj&szf,(Y֢|2ug~SҵF xFTI7-v->O[3!ǝ֏aI!cgZ>Zn}:P5gRTw<75Rs RmH|I>w&]Hz]PltC2f0\`x}{Z65N؍˟<?oh\BQ,sNlps\SewUs_MJ^L$}6%7 tOʮRWC; I5iXUer 3RQ'O[m' NٶO&K?kJ}}-O#,axoBH#RWZuXFK=`vMن>IM] Ǣ8mN xnXOm'~^r.d#>;kv~4UX b=yKu<ݏW=4>f^ hW NK__uJ\mvt_--k DfslHǓNsmyJ "in1);ק=?U5d/5(/þԼS,}ڥ$O lx*~m'RIܪ6)8$|$-f#t 5Jw;}.8 gwANO}/,CCŦ/5R`h }'K"IR/0suh*Nͧ-CHu>=kٝ+k^I=ޭku2S.¤d1dX8b</|c/DklՏsVV̠P3i ޣ2I,$3(]5gYB2.#kԵQP(%z 8ʔq~m\ ג}5}o:H6,+8ەaCƟώ!=>AvBq}۽z1#6~>~_ aW趮j(NH@д P(Z(5( e-k"xƃ -tR3njГ|k<0ai%yݎ4j-6.wa{UUlcS^FgKUU=4XjNN-#U3[i ؊5'c{El] >֎k?m]wxc2}Al1}>'#JXJr׫=uH`DcF:_/%үy_&Ӱ~G?*}21-/StproЫ;jv4} k]gO GtS,jag l_ o,/^OH&?aq3OF<Rupvtoιp9n?-ok34o7x9;eQJ_pQ%N]{0i7>們I$=OF-JI $N"pTAGjqo,pk@nc)Ԏ[gzS)"Ҥt_>۩R3<˷svu]`~ϖMؼeơ< Ğ|M#a}Rk:桩1%ē+D{'|KaO,Nv ݫ#о4Z ;MZ zɬ?_KKLOqkGaCJPq|Ӽ'ඁk"RcHc@ydv?il|/^x7z?,h>`?ƷyUw1V &w Efid<\= Nr);#̵RX%̞NvvUίխ|-[gp9^2SؓxQWKNC)V|@ _ Im5H&װ@&f@kac|߃e2]p%x]F1H-~^B;ap$NM|+0಺Tf\gp:kalU;2GM;|Qg wm"7#9+ޡǖգt|~nW=;3-k[ ?h?=~kO?G!_? _ۘ~Fևe֧FE9a'&'>/ ̧>5Ֆd^eu<vZ %Z**4ѵh<"(V^ o>)%G#=wxV贯W<<}?SZJCK50Na\¶$іDfЭ \:hkz6acGgdn?YZmdj{?Hz (](Ͻ}>p8#OQԙx/ hQZis&yy%vy噎I5b}EXF4&#GVR#uB(ڢn|0KO kv$ξj}vЪw@q yot 7PI#nUyF4{çӵb/0tH<0'O'ӽsN_ u?|/Ujګ;ی8/3 < 8om^4LuGBSV!#^+Ct>RCYJNA9 ݴ$s:u$%Gŷ]%e2|s*~%ۭW'WbkuW ÿ[g@+#s9yUPn[3^V]K/㖆x:7rn!7nx:|} -Ϩ🈟HtS:Cskxj#=|>c%fqEE6!siGTu8lסC )Kŏӧ1σF׋C6L̓qӮz)Ǚ7gMK%Ƨkilh[rx֠4~tE=Eon{Nч>Q3Y$tP1A4 Ô!٠dt046 %oXI?Z&'tTv<{qMEtR$I_x}5{Ai2G<^U3Hv8KK:{;^$n0UQVŕO,;7(K@u!uW.[M8$|]ןIJ2ۨ28*F{QǖEJz_%,]FFcF܌zJJPmXk8oqgKN (bA*zV*jsvτ rޥy_D-ahX6 D<Ό> AjM|uj떣;5_nQ>cmq]Wqlx6i/ O"b# D=p8PyVuM$z>C:ᏈZ;uha*Huv c$F7H8 ۡ'Et8uZ[rҴ]}:7&D {Q4tm}N~뚖t*S MFCgW?*(K<Ċ؃M^3QѼ9n S5uu6|U[> L.v%NvcV)j>VpZƓi+9CZ)l&v pد/ڳ8GYкEݰZa)e?mx{|,.^Gå>tY㰑=crT6sȦm.tjW݈vV-07GUƄVy~"Ղ)Plw.}KkM"я"]ZFpMѠ=;{ƫtDcJ(PqE$*Zؖ?K|M돇?v3ǬyRe'8'5 UUNf:ZʖXdOO]E"5wxcfi:_4]FIV""VAv-خzx[ՍĐUcG{VsNȢQԭFq%V7nIc)SW+,}xw8o2ѷ#'T쭫iK/Px2^Ee% &Bm zJ?ƁؠDdV7.Zp̮M(·}*$fǔ&w{tNO|Sž*Ow0KSjq趬hn3g0 r-_$1PwP"X>S(Yu?(k"xƖ?{\-tPl7O -/bX_iЬmG9*rA#o²> >7{HZiDj UT('϶q[EYXn ?kwZZN$$iXNji-{/ 8|Oz# Xe)oM43iΐ#BbA3;]Ę&բK 6֚I2=HM."Ot;tUEX?^¨&C{⧄e&mq 1?ZpzGxVmJstS]1؇M8ҭG)GU!z:!d 0ξ`Q힕n4nğ e\[ X)jFڕFCq3HTn'%mV;[KBB4"mߵ Xsdm䷂+y3$[# (i0⁓' tE=gWcG.X7k:;렒:c@ @i411@ (X@zޟ>sz]HQWZƭ>wr>4^f$o.U* U֘z.G&Fzr1@I$Щ -܊Uge=AEJv 8#*Su*o#tF?AH,Ou)m;`d~ Aq}ah+,UYj6q#C)*A tMI֕ #41*H# i-$W+'\y2ʟbAڳ5-QJF:lhui&-Hgޚ.-Icr3oϟʣFRqJ7 iWz#(E66=r1ښ|LGG(i]1iiї,{קvm}?ﶥvx \Z dS4Y?[!!huό4bh$+W,Ǎ ?ZǙX?kOYQX W*wn 0}M:nwI@ĠS@C $6!! @1(,Tg,.@_Dʉ$edٙNTE7e7 D3O[[U>VB%|.1Yv/a^NBWϴƋ0!O{\~fZ8[9_p$H{ߵrƺghnXmr~Go[SK.Wifi=)W'@>M;Z  |G>͸@@50b PZvqH 677My8ҁرhzmƥ^)ѫ@HM#QoFd1[]dvRkIZ=>x X3ZǏ~=d#G]xV>4 ̅]9 z`ڮb:@÷?xSӵ̳KS]g,BOa%sRr\zTjOcY*3KKy׍?xD\lwWۆxyVj-?g6PKm2,ȖdR2B dz?ٲ!ݱԨxثǝ^UƗ%k?xN,yzĸ0?DuFͧ?@cs0_~"!VBK; I~̱$OV)ڒc^IJFӑShx Ҕ]ZJHւI6LCs@ (2T@7-ql+J]N [5H /LDdPS(E @ P-HbNp E{de+5<{>^Pz)s%Yiwj[]q$R 2g}j( l7a Q#$F~J|!_=b._+=;8~x-&\7CS;cZOy(K #rԞqYCTy|@Ic]gexTk+[9&#s8p~{j(6m6E R*Aݿ5VE)5P͎lt{Y P|",JvWз4{Y %|-iĂ8#&1} kAvgC4Z֬aRaJ#^s6;>䘳71';Wm(NGu;k6KkwE*?B85Wju V[HnSg@nkϞe#E{RfalU8dFGf{=Z1NZ4r+YH*At"lIWmNY_?u=8$fڪhl[Ʃq+]Ԭ-ͭd+;N;t'=üI,+fv9,O$qͦO|,M&BsܤzF[hz5|-"i\=Tn6|}nĞ 'uw!G@ +ЌyUg?xCTVxmI dgZ/y'O1G1VppkĖ!+Qa"!ٱqs'Ēךv-.-"m'x8aa3o$C#[dhԭCxҢ%[HȦ!*!(h (i!Q`=_3COgtۆ99rˍ\ TOo?B:]Vi!hA3y >W*?'Ώi.X  hG4mѷZl ;j&֯ohWA]a Mj<7BF6t 7\}AvS:8H.׎m--Pgrrp= '韨A=p\J$SSQk|aϼuEhK,x?P|SX[.cd })-~b )s@❄.(τ,m ﷗W=T͂?,okAB?Z Asp9=GJoalc -m+=U9v cF4vImBc+`{'Ək%Skz13Wl|D^ͦIb_pB&Q+  !cb>|s/ vR<NkJHHi _ Bg`H6%ƤciヴFq&Zl /^,n׆}G$() >@?Gi'Xvyo?terO4426[ZĐľ?!\Swe+UxwO/y!? ڄn puOd?k ?ˈH;̟B?Jl'e$/#0UP2IAlC>?xVW`ylET zu>i]yOxg{KrKw#x kIErO渋>Q\{cPȿdՇى~Sy%. ~qEԇ2%6ua*iC' +|Gqr^&/ kZ0I# GR:ZF `2X'3OHG[ͼWT:8FAxU?4R@b?~ULLkLNteܖ-! whf [#Ꮛg[ 1ğW}7x^w5OhvQ}aL '5RK/g4o iuER2I`,eEoIAU:QQ}#\)|n4厛Vh` 7.2pOJ)\^TUiS3wr{r)TXZ B.1V E7O+Ԗ| uܛŚGwb,hsgj]9MX~Դ[<*10#k#~ҝ$t~럈LFEgr">$|J}u;SQ V>{"CmZם-٢ڤſϦ*Z##]Jx+ßu/hF~i-ĺ;nc9ǡw(hxÞ4>xox{eDKdU\Ƞn9 PdՀeO 牯C2D`i8&sVVV)#n >K|_DoZyIxowӏ,lIŭirO?dß kcۇZ׏ZO'8.eVhIV w"yIfwbYI'&_QS`+|RC?FI]w!x.K@v!88ϿJM;ags@?ATs|ri~fjbU: ?Ǝv?I4s}Hv(x?< sLNL( DM$ J69 ͤF3L Rn6mW?E.`7}Pur3QeE4>Ց4]_c Ff<-n1=1 zi<[Hd| q\8c⦤}b%? ,ֶDŽTр(^te?jl`;i' *#?d/~, Keۏιd'YJѯ [;dRҹһ/)# ڋI8|# ]xLkz2+ ɸ{VRJRJH#v;'z1>ծYomDuIyrR,$C+)t Dt~!iWK!c*w52Xݠq^yg͟1BJ"GϹt/Bg(RCv=TQ? FXWF>&QL+_ѭ]AmZ-lyRqdޚ*ZڇL b >7K{6q]r(nmEF Œt+X6V)Yo9$h Ht;csV2VW;O>}:~sHjk:KA>5oL~o;]J<> G#~Fwÿ Ik5Ρ]K{]h:4)h ̒qATGp>8Gs C)PS*D.RҢ$ <y $`&0Ni&Lt SU:.N0k0=7?=]$/Yu‹>.?kC#_ ,&N<_WMY5HG>_ a_w>r|7V,]ɨ\2[V+J(G߄O\7Ha9R f$#-~վ$gtOϘ-d b %AF4 O鐨1Z" -ٺ<9B~?N7kPvV+PZw_F~*xtƏGQJ?[NFI]t6%4kp,X_]XJY\I,P`N4xY5+F;}lJR,*4}o_AaZ{Au!3"95؉g 45ݧ31&\^}Md٢8_ګP0xO^w2+z [οUS>q4-@64W4zљ-:Z?IֻLƎ J UϳWg)|$;֤~ ź!E 8oC>>B(u>|c]$@G֘U͞OA^!eː 929<بڵ2UXcހ>]|  Ic#S0f;}QTېp:ir\W] |_J) !R/py=v*K{̠c=Wv爮M(ۆ?`Lq{cxr.in}_"@ѭt"mmdQ.xI$ORM`zjL?jYxW1m>Gt]4x%C@L=o5xm HK ߦ;VU*;?5kI$qlfg}qXaO'uv%"vOR(߳q+O}Bg&iI'(Ng٪BO]4IgW@W|I0ݭJ"ԏZ>8?9j0EOr14Ry'2QD U.F;~"hsaw~׼gj:MŴ|lZDPOJj-08*G->[ JNv I$p`qʎ7Al:^g/Tcß_Ii^a|9}4_? gxiB9oG؞ ; 펛~UsźoRڦos4}(sqştl}d+k%ҼQm7R_8~_En^@ eyX| LCM@cPw(i}"%Q'I^x.$8V2Ot6Mt'3-}N6|[=[QD~ߏZ-{D>^wG ᦵYni =ꔮ#%i ڭ՜JZ2̼$7 H[|$4 OI?&8Ok>愲#4ˇ?$/j=!_9+#c"#Bÿkg B*'uz %~9x?[,H.h|B:^M B̧ p@s]!e隦ykG it#Y4J>e_+K)huG]ޛDYV>Ϯ"όh#Z#_ ,fZl4gŚ(F %[~ Mko h'+,A# pQ=_F\qJ> Q?‰fEXg/ 6Eykcj'SEK[Pϔ>1H+ן[f`!FGx_T֕?^(^{ệ7d?:ִl\żLJ:ΝρeIjLͥFeU 5E&C1P?e>=5Q&#Ec5/]'F[n譈i~ R|즳1,Ci`ؿdCbOu>5޿ 训ሹ@QΖ4 GC[-KڡWBj=bV{@$_WK_={捥O|څ eRGxϵ>kHQI@l *tmcVicmgNG&V hBU<({Z\x_*h|c f#xˡK _.h];o֓ioբL |O{C?=EY^,q閒4wz/UuB8 eߎ|)f3qM}l sUBnriq1T{@ȊbDj)>xm 5mȉ3cԟuS"%?mVc?g_WgY\Ehi+gFP0/-v4.&GĞ>c9Vu~ϳ넳Ϗů{ @tȭ]C0Z-}xl6Eykc?j'wEK[PϔGZ _iGq<_F=y6Z؃Rk?>:T$3߁~Or-I `IR7Ȯ"j msO=^?BZXxBk?DK4LOċdzj?M`ds@OGەQR?6/-uIgb mz,)7C~0?xcp/x7=-W O\bt^# $#?.? m6n&7?ݘz Ġ--<;WKRF\gK{3Go[OR4|I!.!O'QȆ0E'~(,| 9k_uօ9S!13SI-f*6oUGוY |V6񕀟G%5$&\G~6fOjטaΧ)1/Hq>T%#R}"(;3DO5nkO.A,| g]Wռ{3N٭Z?[=?>kѡ3H =#|փDIQW6 >*%g??OD3Ϫt~]9:ȫJ ʼϚk^u׼VbdxZ$W]FLc~?:kc[3ËJ҆R2eu?-Ϩ딣࿈>xW?5ˆfuB>7Go|L濵QM_]ITLOv}/3i+ˎ:.!!CSKCgJH`A W 5I j+kϜyeb? XbM6/!|=:rϊw}C(i\{VIyS\z+b1Sãe[e#(SB> ع K-uؖxȭmh+4WӞycC{b|F!\>rL??G"#O>$t'QȂIh9PWmjRYAU#h!1*|O I{$v@46:~~PntX8|bi7G+V. )X,U); u"o&#kR,VAgҪ?- c[qώh_*蔮? ,t6)⸕@ZS} E њ~g?4]VHdf4Ԯ\F'|I+Y,& } YE]gly>Cbsh}AMsdwܠZ^OSi򌵝>kZ.Ұ=uz?%kA$aϋ>>>-xLuOD3ւ:_Ǟf8RZRXϼyǍ< xmgȊDdpy:N;8ם&i^KUi_:WӧVf2JNXT99;<|jOH&?/WE CTVo4e n}IWUe)VҹV %X '#vYWFCCz#KvZ_K Ť-,vWiMmC>Ks` WY'ܟ tK9Av] yBϴ>*i6R4<{_<S]R7nd Q]|#Ο@c'zM]}W.i_RⲺ3 8,@,Iz{Wzd(JHRg> 񐵴u/3K=뺜g?lvWp!\6rkcGm40]-)PFk5.!K,wQ[цF}z胁5`{geBox\lf{G,W !Rs6>w!}||Mw<Ƴ:'s8 mַQ+$rZӌ -GM[p^ '3*U}GKiY?%??G8Bu?--;<|jMǏzpM aAamWp2O[M#y1g19o_ƚgZQHb#W:_ *T,/c-sKO#X]4 y &V71/Cgy G;}~_߼~QP;_٤Zη5Qs_Y蔮_ 'ߍ/Su|r.e`|c$y_D|#OIlxs{ugn-m晤,Ԝ+)d}?i tbˉ.|ˆ9:;ho tֿ 5 3C})xLw @t|&i$kᦙJ@>ppOe`Ҥ>I~fDQ2;=$ow\v(G𖣧^SI1(ȊEEM靹}l&x56H*pA3꿄tr-?ŗں h=ArT֨w=._V|M!j!T((+P-}(3NfIv ׁXڜ #%Ga\㗍mԸgZ vK6;dWU(8GQ\xBRXDkRpg\©${jSs`u~ўVa8R%D5/5 /DbO;Jf@Ӝ18|M?l  (#k=-z)?fc< &w6?WC?⹑m7AdkyX{ɃT$#ʴf}7qkq xdl;g8|nǍuWYAT{8F~<$~F ~=5V^^XVC ҩ++!QIoZF{"zK) ^^=ښ:,:4XӲO,ZY],ēCH g8敆 E98ui<+gjL_T.L3|5Eo?hJApYϥ7E?>_U.Yqu⨴Fb:X^KӴS/7}&e#ᝀQh ? OG`B mf?\cëQV?TY%<÷n1}ip=WܚgHZa5 >ƲKk|K+Q(L]疲:;F & [^!;|D1's ,aivd>#Җ5[rFv);s':qIյY-.SkWʐrA8i!\6ZfsmjQv*s #.=2\omD7mf\tu#8N}hND(3lev)֡O~.m5^:n]$6?{rq3hoo_<5ǥh7[iF=ٙI'@*\i*sߎ1O =@сh$hʿG=~aU/h>gB ?=ş}T=O{ߪ`Q/G-v)%s )6*(hCUҴ{[6w܁4qp?.xg.x k|GrzjWs}i7OLbK$Ky{F@ԐZzD T;1/X7(XC/ދ,0I :-ց L4ƱG O;O5װkJ%E)xT𠴋RqR' -GSMޫogıD J&L5j8io.EwHߴ N=)-]vw\ib`[-AQĤ}܂$vb|񆹤_֚o,;5fqcp1] jOsk(}=LvjBd2)rI=N)K_m>,K#$!\mۿrym ܇Wh:_TUl]mh@/ΩA+On#mNKXUʮNN}+9U rI͡Rpiiq3rp;9šݑNijZwKAn1;1Ԣ̶3kRƥ745 JeDiUV\=Zx4y>y.S߽ӯJ|WWy_/~ۯjk+i_\bv8**TJBk8zyrZ_M;*#'T"Fb^ B + R5xNt,՟!d]B}VؾJ7c2iB9 1ԣRW7M8<i{jJ|KQ|lK,ZT:jv*5+;S~Rr f_fb5Iūeinj5>kcWj\~-?".t;?.t+ |s_G: <Ϙ4!έ=ծSHqqLB ZB9KIo0oCA4OpZCZ7c'T!^ǛFG* N? PYz‹ g~WS@fz֧*=c œSJ) pSz})X4OW3`,]B?8k`+)uXeeY i rQ0{H>*QHGZ@Z"gЋ_7?4KrxwH?v?4y"j^]@~'0 _Fz3<^#17 ݟ|pL)@1,ɌEt:?NͦVy/z1Q>|o^;^}^?8:+,yƸ:_iG+S;ZS+H2YV}WtP_דIU?#]ԭJ>=Yzׯx+s&=c^Z1<|oY>'?T_9Qq5kr]oG* VÛ(ApZ>T[sThne&!JW20Hlm1 (E[4nt]*&W`xe-Dq4]xMn YrpsAM;<7k..[^4G2UJG\x);z[G R#vg隖\<_◃d?Ҍ{fA=0Z6$ ?Lm7"Q?t "u+ 6cm aZM19#Hpg:򄞈޼h-/^aXN20>ISqvƚ?o|BE.*A;G@JS+G7uC_hR][I[IPr 8 qF8;#[:Xv-C ?hgl.sں{KZfriw~~jZ7~$Ӽ*`/;$i\1(<\g+4Ce=#θ&i1<CJG]sħ%HFYcOJLr8eqWf[/ |C1넟һsg7%z1ٽ/?Fz3W:1^nA9=_X_4OL_hrħ[oigĦci >ԟ-KrYo_ʿW7{tԿ-y/?Q/?3Fꎌ:Xe|?Uם?h'B5acyvZig`+^cuŕCR3 6i?:i*>bLP Hc 1 (E4 Zs@m,n՚œ@EKs  unh&@B~xk^ 88^v;Gfj'_ ,-|W57L쬉8#7R'na<\z[k)fq2zWfV3Q )dbȜv٫[ZhuVPs"ٳ"̨+ymwokeE͕ӲV # k.ji2s,hςmd8{(ϋ 4-'>0| ]KmIUvzrJЎdЅtfGSj'Z?z|,֩].˹1[E+U?փ=nltMbl!y_iVc>ֱٍ>Hz5ދ{=~6'dgВlvo<ϤWK;Ds)ɴ3?}Nk?I}ԫO(RV;j|9!Ŷ?SQKW>o>@~JGG=.#GGd_~*c?[G=./+g})J?)-p1QL$ݝTy/vr{?浶FeF1TywgZO%ԣ#_뱻M?2)YoYT~JƖ{Zh$Ҕ /ݤm^?^{o"B?AY_k}cOv_Çn$O,JeцcXgPO(?S/C'z|Ipc??R+4$Q.jz-׶1]Ucf;R1\40`&hUP1)LB c"E4 gsU9N iz/JXV`\sqbNCzSQZ-_Oik-(&3Q܌Gp~Z:s5+ϊ.n 'M2eg窯'=3sBƑmh|;M<@\RThWCޙ8%vZHBOBxZѕ(#JRz~8!⁞GiTW&?3jrErFx`4֭ۤOQxoO>\EY'[}Q*w 9 =ӧʵX6@ k7qb1_ݷWtBGpD./.zT{(]PlG\knkx@=B-?T{o,?U!p_,æ QbqQ~Ņ(?K k|g196Y/13(xf +zH,Oo65Q>IOvomys:Jh4p:lW-Jn/;mgZ>"֗i}圧bC*Skam3ǺE13)A*>us~f$9$IGQ?F~IbkYwglgƊ?O]W3&yk(ڄ)O >?0ߋ 9?A9PZ .ݏLYw͚_ֆo%q 3+$_|(]觯W^K_ϖ<-eV,eb]JWS֐MŜ?Bj1/.\Qjt}N<^ Qc??Qt&şu_[=tt6`lVpܩlq2:`#`6MZZ7=iZ @H?-m;ǹR۽JgOa;̪5a >OƘ ak4g"D;-luoJxO[hC-s|OVw|'uO]{k4]{.?-s|'t|)σ.'"7Ot>-iG\[/t+6G?Ƌ؋Xli Ç5S.t<3ϯ>G2 #:Ɨ2 #G2[Y3hWY3h "?"/tMk|_O5oƎd+1?zOZ?\S-k|",SY?G2 1ָ?d?i2Kng5<kX?ƎdA l,O̅f/&8Le]Cz)AC٥Y9KFf">Ӡ™fVTңNRJܚm+ҭX`3iF((xN/\C`;net8ǭp5)*$ Y2Kcp66G:+.FoBj8hd8du]^˨.g$,9-0)ƜcTR~xT1h{ug(}q*)ХGqLEJaa<]/́SBzfB;ZU=X6Q]ۗ 7cps jB|_Kc+ffI$I9Jȗ.gvMgrKPYpOC|So19 sbjR]sHS1@2:`h(hR(9(>Sjot+]Z ź[Jc98Zn#9~!_Vo?/VQg%~m4rwm rŸwo}5 hG+?oWm`~!ϕG(4C;_{h ˝=4 ZK}-wvo.!gh 7nKHJ3[y nj_٢N 9@xehZA9@cxhZ<[Z-Q<}v[j$ǭU>T-YxR++3$G9?(.OxU.Žōr,x$RI=3ځ[A7r"yy=?&\.ǚi7A G?gT{4R}١nzoVS;ڡ;*&*<|CԻY~MQ`6#ofm-cw djgent7H=TR0Ҷ$ (@\Є4P0.=(i3`Z|H\/G ^1Fڛܥb!s&8$ 3}EJF79z_ Ma48$QvjϞ7^x]lNș LӋcPm\~K9x^Ñk aՕ g!9yV;?tJ=6 ^UfЊ2RW@јW@4O-A@ Ml cH;sRo? [[qՔ"(UN hUL%e֒ؔi|RoRr9VK;*O t -3Оxߞ))l2tƚ`% jXCWˈ[)}+‰nxOW3? \%1\y#'Ώ3Gz-C>gpa1(S.2K3ܟt"ԫܿᘼ{fՐ8\ )F@NuMMEHT/sHUڝZ݈c9iGT:eWzy:3}*L"gnd3"]C&; 2:ǥgRm-xhDK5R3 ]/&sŪ 4cncFD< d/Y97? t{*ZO*_m<{uhOe'~MoJ;+ \@ s}Wdn1L<^\}v|Rn_?R4ia$at('d)Yأ k7>lu[&=E}?ZJĞYL;y5J-hg-+Ñx_WY7H]srHnJ)5%W4[5SwZeDyC0aޓ{<[oo kWFXɎ%P8TvR-8euOa~'N5v?iC{>ծaR2<ʡ ď|5]|s:>#Y1SKak?/tgjv͵{*c5I[ eQ}1ɍ!OYOXӖ|GUvi_+TDgiv7:VQgT3* LJ|'crW>?b(u;K4:-2]J쑌1N0lW"+kؗшW|n<0<Jj\.5vγu#NⱑXIj0U&;!_>,mE,T}8Jrpt|t$y g8 :߆o_,*..YS)+R7?3 c/J?*>RLgTG _AwahZVy!&[~2tF,nkd#$sOã5@KH?ڹn6|k Z(84r `% }?,ȟJ1;ޗSwq5R*͟xRy%m?Is!^~'Iu V)^ m.=:Nj v_Gc^OOxA| 01֛!_dо5I&I$90WP}AqwwMMڵ͝֏?XO.R܏Tiz'3ׯ,k?on/3xQZXp|>~5F/nd!3q^ݎo3^O"(R aInpcW@-9U*ϰXj]m`$|c*Z!<ț$cU)!N)˱%?Bk[^urH;}zNKg ͝ϔ.",!g XqMµޣ%O{IzWiXed[oz֙xJ 6?*o@7BXd{=ǥsOR9yI5f}O𾆐.v^gw>;Ϳ Cyqg+7HX| z!>$FkQmb3D_QŽǢ4:  Wl?*`=?Hχ݌xHw P-"8 Ծ md<,UqV;iCpoJзPQxUGB蚺dX~+wUQbp<=rcKN}[$cؓ35g~Mj`e*Riҕ`<}0;qD7=6(nVy_j$l~JeU< >#Gk:ޫ|73OC@)g|=Vܒp̊Հ8~2Zj&e0aWB[nw8~;GxhNj㰌bx0;^C-c`rl zgҳ.T~-kW\-~mۡE+w~H }EK̖G=A4 F$qe%gb|B Ox[8O&#M (bNrG5NW@?OcƆ+3LЎ\:|dh鞕R>4O 5;c"F;c]'a3noP>`3_À~ܞj騿vڳEW= ϗ[tJZ Zr!₈B W7VҸH_g. [P)x/Qƾ~6BkXSwȉA=;^3dY]s6 3$Di:M7W^2uzN)j`9X ›iMnυoyS zՖB)ge 7x` _ x2 HÁaarbZ1yχ~_G~߆mJȍAۼ$#GT~CȌ.#BAˇ=ⲣ y3 cL5;;VݶI?wpGJZS ;lG>vcnYOVʜ|W+& 9t5KV u9WpX_KlzSx?k5j=GDѬVD4m& #韩)a*~oYNamZoKjm,cH`XJ?ޞ'.kƳN15+HWLXS5Q{k|!#G\#$0~OJxc:&yëm'edJO!azx 88_4YϺh:vk%G ȃЃ8OT#>51ψ4Δ=q[Xji6rG?o+xtvmZVtQE桻u(h(rBpC Hn4P4c?ODJ|Hxs?_ڗ„KLk(7,QVjV^O2aqfQ ]ahCYɩ {5TNM>Mf-v=(W$wuN9goǰ$RUƚ3 .&A9Ӹzq9B.%Wú/\6m>N.&$Nv-ң$K_j|EqLhQXV*$ ա7RsrBp MOu,\x}[}h;S'c;});GrkHK7_^\j0Kc4*$;**6a<8w/Ar׺bW.Y1Ǩ52n |qLwGbk c/9pr0k ʓh7nZ/*ݰqʰ{9SRW@'Tv_D7R,~8V#??A$/XW24J2Cfq5oc5^z̹oΐ,iCrxk\ݏHu=E\G}D՝\ȗ6#v#ұ:C_׮--پ|1'j4Me{RUE1:O'9[l|B?4o$c}+ោ+{{;lrpN@̚rc<{kZi,WVW^pu>R|#˔xiŠ<ieGyX,NI'&Jw?]_fmSWH0[U WFڹn>cau6(B@PqHc[1 4 @1)P_%j?*:|E#~2;_|Os+ M{m9 Hd grxumAuWS|7q{S爃M9դ{(!GaWRJBSϾ;x?md/i`Js;rFӧU~=7V*x"[:l"1fB('"Q:j6WZezp"JpktQ ƒDG&<k\|A^-%-eq7=z_RN9|V;.xR.N,QG;bF<LFrNQ?gáSܱѵ o/%>SV7W(oŭ:ol٦q,d':OG}O&¼Vx/b7a2=2Yx+[^uߞ>5?k0i-]Hyc\QMLbY;F$hIv$~pH)\Ϋ`l/Y~&[G\K<ﺘtP`RzҜLާxKMF|o(|$NVq\c6h_[[-Ip0#~iys1kw_ ĚNu+gD}W|,'|$Ӽ["yFΌ}k7& `cG-Uek nxR\O0FK#DAf<k@/ >iS[eC:C 8NR4lkm Ş ,vݐ|޵9u|!!|0(_7o&8NNY09$}kKOXm~ٷv~X5l:;ҶKbY쿳Df9,37tRW>##ؼ9Wc.34|$|kFDx:b=kzUr+;FNpO2WǺdJYܹ(@eώLld|ÚFd>0{ք"ÿj~:OdKa=O=s>iiKgQͰy`.6WQ?t?TsU{xSŞ-K8A:]H zVqnHGWҴ=E$g<ƼFӱ9JE3L{~t٩lh1=@ȓн េkZO UsN{|yz8+?I\؎ 597ֽ57Q\iVe5gF@`Oj,귿lhЁ%`-;:}'G̸ȓ*Ix]>xE1xLrx3D¯|c5bb[0 ,/M [\ K :ccD,vE_渺ox JsJMIJsgɂw&8RsvB>y5xLYcXTGszZA]_lʒ@_?־NR䡷sYD)Ǟc3CmA⼻֨'R:z]YښtAse)ϳjR'zsP8#s)4xK-5_qwM$|aԣ#OJ 4⯇6qޢķ=WGڹ|E1u1Ec-(C@!4iBJzvZS ÞC`ʹUh"70gVŨZDj(ʇb:‡7h>V& mU( Z6#[t7 k3Z[)*"Gbs*gJ>dkV7EIiZ,V'RZl@ gш_>VtٴmfLwFJNAX=Mco~""6庻>z9ue'ر.mA/y<=Bi !jeP3i>wxO]%uzcN`ORg'uhE*hïe̅c ^:GOK[ 'J]5\+neќں"G>zDOI P V$ niӾ!" ĺF=$6 9%خc5lr؆[?PΘLĚZ;jox#Z5t",|3˱x2Nxgx"C&>c(}5c*L5Qζ0쾀} \梹bI_Wz7{Bv"MEtQ/sPgxxoT,'Ͷ`@JAֹǖWCG|!sikR|Iks+)|?Cq]<]@"*T0{D8竑|9\uVR.#w?t%a%33x'=E?*j?e#[W^#"' ,g!i%q3־0x3FWOU`$sl áVPOB| ĽKXy;|D/ еK!E̊0 `>˜qXFZ&_$l+tyn Wtך/4xg:Lp^&FǃA<_i _M:iBsp/BNAqꢈMC_uo7q[=2nXfsv뛌,gu/GҴ bE_ $ycLl!ZZI}X'8G~|To]5GCG\MiuŬ xF! 5׆5-㟂Fn@oO5px 3ctW~#݌Ͳᣚ5(=:zzѸO|h>7N34(k7{\\%pg]tq`?&C9:Pw bpݵzPWS\0ɲ8-^ :M[y%.8spV}EĿڗem/[SenpنFEo)l1hqπ|#s:ewssrpy#nRg̫4uDw2݌! <8_c` 6~.G{ioះ&RUm>b{SRw^о xN_TmnH\1D{`*g7Qo^;P[ˁ2֪r&zR{L!ʆsQ=mI*,ѷg }Q M3[wD !^T{j }K^[\߉fL~U@x zz !B.VjO(;OڝƑX[0&_޼#(n=J6[|? xzBڦiĹ^GLlN&.H'%9];r/ģ+$g2~+K׷[gͭ]?YG>7k >;WS@(Bs@2EPJH! (_'[EG\?/x "]%>+A^Ci[B~YP>~\2\ßosk?:dp2K{ 5wMsx;]v-5w a\R-8#Һc8h#@|R(wP0e8ϬcGτvtox\kΚUˬ鷣9_a85 XPhzG "ig,kemԸT~aWϹyI`| W^4Uޱx6 HaDQMt* 9U?|c= haߵp 覓j:ZiCBOwfkGe0wLcsnd>|<,noMGL|'s[\8йo)qi?bU${*!5"l`̎2GB hIψ^9Au,r<`tQIA_K&7^ T*)n ]Oh6OC5'"5>$.5F4=w7R]ȥbP۠R89Ӛ4}U huCPbu#=RI \L?ʙYYDzמ L?K̭~5'}~OD _GpgY.l-elcAԆؓ|GM{_ rEJ <o皮Tjv,ޒd|聑 o^j/$c?1D~7h)FM ?xq=V" Eƾ[˷+mj)zR{\!2/x#X| D B, Oҹwk=ַq<7I ڋv=g.ǝnFW\jY!JD54CKl^X؏1%6gڳc)Uy|U)PץEZuZt|D^tcqs@#8S0n.i%RtM}_fk:Ȓ,@21x9=+8s18<:Irkw׈4 < X}qm(ШOcl%yu8w%ܶQr;;mo'G>w9UAtSi q^_1_\4q+~J? `ϛu2HȠa@4pcRBZpfdž|=mQ4%c{FtWpAIv \ٛOVeF "{Zlv1Eϭi7GR_ņGO2Ec4Sk^ԅx9Ǚ9GCb*e-}->$Km:@[flL׉~x/] =i2@ >#9o٣AY\Ԛ<#SRS| &)5 (QHVOD"/Osxe-/S+(.Oӭ ?xZouǝ|=z{]rOk}f.# apGQ9rW,x/$wgڼ%ȋϔ~UN;)Y\f^x?6O[f11 93*!oO=S/)RMJD]G$)z'/^/έj2Zk!0@۟=k\$E)NNɯWi~I[$|'J5$ ]_v7RE<&Cޤv5 ET A*KC+e##nd{oFӼn"Q.5A۽c:#=>!Z6]aqc IT ٫/~ g'?in Df=To k Z1 G]N"G?K-Z]]&BiaQ}j9I3?xYo5ݩͬdGa\怪W*;S_1׺MSxoXZHPb^Dx}zT̬I#ZxWTqa{nx8!}fv ʒ+s ~oClBaQup{,HA9/Յi7d#?ioWM e_5ZT9Tr"nL8Ɯ5<[Dtkqr?ύ<#{MbTI#Xދ0?POpufKo1a6p~F 2f5@uេ]שH̞F2I 5<#xS.o{\E ;95Ь}OL|Ab$[L|ŇSr;kX/wEƣkolMfVXǻg)RʾO^}1Zoد /4,\T6#5\SϞq䓵/:RitQ#UN=ϭcV'7gM*MS[#4[Il4WIuK+=SNS9)' NgwvwfgbK3}II-2:M#1`EѵG(d{9&)qZ|A5mJKa3{jib;B@ܡ#Rz C@( @4cҁs;K9s^Rs!|1r;[mT$z*IWuu>w% lp[HG#L d O=#fG{֩<& kIťG[]ϵd3{ ҼWG}+U`z.aKYCRJA@ 4(Ԑ1TM֐ h.=ɉ**ksFh[N#o"7b'3Y}y?mX+drH/mkWH0⽠g8eP,a̟0?%"_73a-Ǚê]'ȮF kXw,<~C۾`?u 0v<?/l7zOrl oIQaX_f_I#SXp_C>6ǧf!`'9G}g3Y^/ 7C;lOo7=Xc~㷊=* ?o+/n>%?K0?z=߳oO۾4Pv$?_X}o3YO]X(0'.y?]LeX >'l׏lcCH 9$hp_?I׺OpkWq7dl<z#VVcپ|O)) ?g >$-Aa,xA!-{xa]/>a9Zf1`706A.*,Ml98U'3 W ?X}?g+y =X~Κ^93\i{w,1g ~_܏n~Ir~?j~ f?j=g;LqI.n63MCSź Nj|FtU7kn#"cۑ[Oֺ!.h6anlch4M[L>o.x&< vti i0i~nQt6T=>Ƴ$݂Ǔ|iKFȑzj~ZDBD Z\PZw$ i`Fi@(,'VG"V Ɩ}]>1l5QodֲdzqsʛEieaU B@P ?_kM{j%PI(xjIϏq xGJ=>8iRjsK~nW.c=9SHG2|{_bCQ?|wAh={/CQqx{>VjB)kOwH.CXwۈҤ݂$+o&x ̈I p޽3=>-W30ot G`|^`(q#5Kx;nϻI&eTaFpVYtk05K ,*!:zq5E[-X0דG,Dãt~+IbI0}4Ւ,IPQqLCP8Ζ:+Y3{(gc6t*JGb7X+En, &=D kN{6PXS<Q9(AY|cESB8<'g sS3N_4mCXk-dkhZPƸzuC\JaSൂ #g/$؏J9T_\Zx~;ty|/nd;?*r|e+x?҅B ??y l1):0v3Z=iФ]ޒp(j>1hZ[Enn`g*F)QҁM@}i4 p\E$1 CM%P@ (hU8 7R{ 4GeqIhtKSE-=?5e~ U?Uʷ>*X}>D0e}OPf>%ȍQB5|Z3u[eI$xx{JJ@s[> HLFS1'<3ܟjQqZֵ&{NC,@8o] "!Ϩ=kҾ.w7O;]\ˣM;M1vk.Ib2TiTɗ IG-^~ѥཿ= x?9& /ZOd\  l? |U}طoX*soRn|ounb&nY8׭tBx+AlHϭz^ՠѢr](ǖqܜʤ\u;nuK˛{dy!ȍY ­l2aL@E.#֘xTc @L<{ߺ9\cxY<T RU* f(C1.gkI1P]fQ@4 ((!1@ m 43@ @ .h&hs@h `% 3@1hJ`(4 pjAp>!3@ p41s@! vh 43H0 4B@!6 v(P @bPPhE L CIJ -6%41(AC-!8恀4A 3@Є0º"jze޵‹ fݙKlz6ua rB@A* S2 /A'$g;a)7d}*Sz>5k[kۥL2xOSo톟|mwlJe 䏕/L vK:S[$Bcٸpb<ɃZ ۚK!t0*d Nqtz[Gk5+㹆ub I# ` L7tna2${ky y)#Hv aR`~tN#kvP'9Ҕfew#idC7fI1.}< ĺڈyAslqBqV5|Y űeFW04计 dw1w 3>eow#|c3!$e6Š:P1h@Š4B⁆( Z(4P(BwH ( @ 4@4J(@ @m( B9BS  `%Rhh JQ@ LP=P@!@ @ PP1q@((S$D1@% - %C@ @-5P! Cց (0uҀhP @ @ @%(4P4(@:hE PL((@i ZCPi@ (@jumb)jumdcbor8qc2pa.ingredientqcborhdc:titlejimage.webpidc:formatdWEBPjinstanceIDx,xmp:iid:9d44f25b-686d-4cfe-b51e-20a51ec12296mc2pa_manifestcurlx>self#jumbf=/c2pa/urn:uuid:85d00832-fd1f-4d84-bd31-163bbaedcfa3calgfsha256dhashX ˉl:GR+V@-J(nExlrelationshiphparentOfithumbnailcurlx9self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpegdhashX '< YE7n)76w;mMZPTt jumb(jumdcbor8qc2pa.hash.datacborjexclusionsestart׆flength!dnamenjumbf manifestcalgfsha256dhashX c2wobnlb]ҭ$ ZcpadH jumb$jumdc2cl8qc2pa.claimcborhdc:titlejimage.webpidc:formatdwebpjinstanceIDx,xmp:iid:2ef550d1-a748-4d9c-a5b0-39fdaad55f86oclaim_generatorvChatGPT c2pa-rs/0.31.3tclaim_generator_infoisignaturexself#jumbf=c2pa.signaturejassertionscurlx9self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpegdhashX '< YE7n)76w;mMZPTt ͢curlx*self#jumbf=c2pa.assertions/c2pa.ingredientdhashX {kM.k& {{` curlx)self#jumbf=c2pa.assertions/c2pa.hash.datadhashX 1+ـb63jr$AΧ?TGcalgfsha2566jumb(jumdc2cs8qc2pa.signature5cbor҄Y&!Y-0)0NIknAuM0  *H  0J10U WebClaimSigningCA1 0 U Lens10U Truepic1 0 UUS0 240130153536Z 250129153535Z0V1 0 UUS10 U OpenAI10U ChatGPT1$0"U Truepic Lens CLI in ChatGPT0Y0*H=*H=BnrSRPkE16kh^׷Pd퓥j^ ^= '?6%g00 U00U#0ZkfӔA} {]sKK0M+A0?0=+01http://va.truepic.com/ejbca/publicweb/status/ocsp0U% 0 +0U dݯ^2Lr ohs0U0  *H  H|ڭqAcV2Fp' t r:g;A>=4 C$ln-~meMnZ# *A.{=nsH $ xηpGzKx$Y~0z0bỉP:_҂(0  *H  0?10 U RootCA1 0 U Lens10U Truepic1 0 UUS0 211209203946Z 261208203945Z0J10U WebClaimSigningCA1 0 U Lens10U Truepic1 0 UUS0"0  *H 0 çPkjr3eA`(k "ŧ (b; yeyXɭBa]CPoAl%] i*+68k?~mPw&G8JK="?Ro;^9t.o#%3`s߽> 9.wNBļ-A"TL\U݄@hi䲻JKu]s,Dfa'qP#!Q6Vx UtS30IL#7<>IoLlH{Qߌ|i21&! Zp6)om )xUa*{P\Db݂'ѢfsigTstitstTokenscvalY?0;002 *H #010  `He0 *H  rp0n `Hl010  `He V[#`^0 VjG3mO1@nSX'20240701150349Z?e<Ͷ6 00D9?_a0  *H  0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA0 230714000000Z 341013235959Z0H1 0 UUS10U DigiCert, Inc.1 0UDigiCert Timestamp 20230"0  *H 0 SE[>T#ϟ] /Hz;*gbXͪj)bciX5q:P ǚ;/fii[+ P0hʃB $j;]E alq^<.yfR>_CӄH-^EuuRGx)9kxYD+JՕdM#ʆ!dpc.$_v}1eGUJ$/+{s>2R4ԻԠ,4nd7QͪLfhbAxmXAر,Qbi|dM^Pɳʼ;hD;Bs} y4~\ XL>iuǃdu͏vV$k!4/:k*{R8 qlq>oaG l$Bʠq=ip' O6_p .d"+(!IQ~f;8QʔP:ӊ@{00U0 U00U% 0 +0 U 00g 0  `Hl0U#0mM/s)v/uj o0UdVe1I0ZUS0Q0OMKIhttp://crl3.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crl0+00$+0http://ocsp.digicert.com0X+0Lhttp://cacerts.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crt0  *H  ޠpO_B֏ѪUㆿ',AК3J6Թr~y8H_=2u6gZO5<*lyD:8;^9X|s1U ~yeh";뚂5W(i2:Fkwlls:IF̶8C,NL}hpw \`(8RZ֬"#NPkwqDAɸFl2|X/gGesk,FA_٭DA0067$T|G(f*^[0  *H  0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40 220323000000Z 370322235959Z0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA0"0  *H 0 Ɔ5I=rIQU%7Q҃ўLm̃ZDB_h} 3P &smW}Cs+"=+>BgQ=V(-ӱue)iِF{DA|jWz7y]dRvGa_T !hn7!@_J}9gcl6 \dt@rźNXMy׏s,9H1W)'.NvU&p&G CCc{un'%:8;["ق*ǒ>sZlR+Xt@(sCJk8)ʪsBhF:^KvQɌ ;["&}_#dc>t? v]Fu`X (T]^0Fvk 3ͱ]0Y0U00UmM/s)v/uj o0U#0q]dL.g?纘O0U0U% 0 +0w+k0i0$+0http://ocsp.digicert.com0A+05http://cacerts.digicert.com/DigiCertTrustedRootG4.crt0CU<0:08642http://crl3.digicert.com/DigiCertTrustedRootG4.crl0 U 00g 0  `Hl0  *H  }YoD"~f!B.M0SοP]K)p )ii>` \[m %41gͶoPLb Vs"%Εi?GwrtO,zC_`Of,d&l|p |屮uOZ](TՊqver#'D'$&*yV Ečrjq Ķ͇$OIwfrKR7~S;I9z%c',=?kfAO@!!@з$x:䞭4q&k8sO?;xLĕ{ _39Axz8#(_+~Fu,',&o{6Yp7 O'`gfU:)+A:1b  Wټ2]# v&evB) G+UT++/DJ78+|00u-P@Z0  *H  0e1 0 UUS10U  DigiCert Inc10U www.digicert.com1$0"UDigiCert Assured ID Root CA0 220801000000Z 311109235959Z0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40"0  *H 0 sh޻]J<0"0i3§%.!=Y)=Xvͮ{ 08VƗmy_pUA2s*n|!LԼu]xf:1D3@ZI橠gݤ'O9X$\Fdivv=Y]BvizHftKc:=E%D+~am3K}Ï!Ռp,A`cDvb~d3щίCw !T)%lRQGt&Auz_?ɼA[P1r" |Lu?c!_ QkoOE_ ~ &i/-٩:060U00Uq]dL.g?纘O0U#0E뢯˂1-Q!m0U0y+m0k0$+0http://ocsp.digicert.com0C+07http://cacerts.digicert.com/DigiCertAssuredIDRootCA.crt0EU>0<0:864http://crl3.digicert.com/DigiCertAssuredIDRootCA.crl0U  00U 0  *H  pC\U8_t=W,^"iT"wmJz/-8r$RN*-V0z^CDC!rH˝Ow'DY/ 4<LJL@5FjiTV=wZ\ToP=v ho 5` X@cŘ"YUk'lvo#-~qj#k"T-'~:𶇖[\MsW^(⹔1v0r0w0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CAD9?_a0  `He0 *H  1  *H  0 *H  1 240701150349Z0+ *H   1000f+2]ΪO@0/ *H  1" O6iUݫAU"q.ǵ07 *H  /1(0&0$0" mt"@WhA6oU3M x(0  *H qD@lL߳&Gp8"s}Y;@5*e|uDEa5RiSq=MB&#q66CŢy,s=xM>H HK XXjXhUL9Nw)3TiMnAԧTiRS:hApatchkit-0.2.1/src/ed.rs000064400000000000000000000170361046102023000131670ustar 00000000000000//! Parsing of ed-style patches /// A patch in the ed format. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EdPatch { /// The hunks in the patch. pub hunks: Vec, } impl crate::ContentPatch for EdPatch { fn apply_exact(&self, orig: &[u8]) -> Result, crate::ApplyError> { let lines = splitlines(orig).collect::>(); let result = self .apply(&lines) .map_err(|e| crate::ApplyError::Conflict(e))?; Ok(result) } } impl EdPatch { /// Apply the patch to the data. pub fn apply(&self, data: &[&[u8]]) -> Result, String> { let mut data = data.to_vec(); for hunk in &self.hunks { match hunk { EdHunk::Remove(start, end, expected) | EdHunk::Change(start, end, expected, _) => { assert_eq!(start, end); let existing = match data.get(start - 1) { Some(existing) => existing, None => return Err(format!("line {} does not exist", start).into()), }; if existing != expected { return Err(format!( "line {} does not match expected: {:?} != {:?}", start, String::from_utf8_lossy(existing).to_string(), String::from_utf8_lossy(expected).to_string(), )); } data.remove(start - 1); } _ => {} } match hunk { EdHunk::Add(start, end, added) | EdHunk::Change(start, end, _, added) => { assert_eq!(start, end); data.insert(start - 1, added); } _ => {} } } Ok(data.concat()) } } /// A hunk in an ed patch. #[derive(Clone, Debug, PartialEq, Eq)] pub enum EdHunk { /// Add lines. Add(usize, usize, Vec), /// Remove lines. Remove(usize, usize, Vec), /// Change lines Change(usize, usize, Vec, Vec), } /// Parse a hunk header. pub fn parse_hunk_header(line: &[u8]) -> Option<(char, usize, usize)> { let cap = lazy_regex::BytesRegex::new("(\\d+)([adc])(\\d+)\n") .unwrap() .captures(line)?; let start = std::str::from_utf8(cap.get(1).unwrap().as_bytes()) .ok()? .parse() .ok()?; let cmd = std::str::from_utf8(cap.get(2).unwrap().as_bytes()) .ok()? .chars() .next()?; let end = std::str::from_utf8(cap.get(3).unwrap().as_bytes()) .ok()? .parse() .ok()?; Some((cmd, start, end)) } #[cfg(test)] mod parse_hunk_header_tests { use super::*; #[test] fn test_parse_hunk_header() { assert_eq!(parse_hunk_header(b"5a10\n"), Some(('a', 5, 10))); assert_eq!(parse_hunk_header(b"5d10\n"), Some(('d', 5, 10))); assert_eq!(parse_hunk_header(b"5c10\n"), Some(('c', 5, 10))); assert_eq!(parse_hunk_header(b"5a\n"), None); assert_eq!(parse_hunk_header(b"a10\n"), None); assert_eq!(parse_hunk_header(b"5\n"), None); assert_eq!(parse_hunk_header(b"a\n"), None); assert_eq!(parse_hunk_header(b"\n"), None); } } /// Parse a line in a hunk. pub fn parse_hunk_line<'a>(prefix: &[u8], line: &'a [u8]) -> Option<&'a [u8]> { if line.starts_with(prefix) { Some(&line[prefix.len()..]) } else { None } } /// Split lines but preserve trailing newlines pub fn splitlines(data: &[u8]) -> impl Iterator { let mut start = 0; let mut end = 0; std::iter::from_fn(move || loop { if end == data.len() { if start == end { return None; } let line = &data[start..end]; start = end; return Some(line); } let c = data[end]; end += 1; if c == b'\n' { let line = &data[start..end]; start = end; return Some(line); } }) } impl EdPatch { /// Parse a patch in the ed format. pub fn parse_patch(patch: &[u8]) -> Result> { let mut hunks = Vec::new(); let mut lines = splitlines(patch); while let Some(line) = lines.next() { if line.is_empty() { continue; } let (cmd, start, end) = match parse_hunk_header(line) { Some((cmd, start, end)) => (cmd, start, end), None => return Err(line.to_vec()), }; let hunk = match cmd { 'a' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"> ", line).ok_or_else(|| line.to_vec())?; EdHunk::Add(start, end, data.to_vec()) } 'd' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"< ", line).ok_or_else(|| line.to_vec())?; EdHunk::Remove(start, end, data.to_vec()) } 'c' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"< ", line).ok_or_else(|| line.to_vec())?; if let Some(line) = lines.next() { if line != b"---\n" { return Err(line.to_vec()); } } else { return Err(line.to_vec()); } let line = lines.next().ok_or_else(|| line.to_vec())?; let data2 = parse_hunk_line(b"> ", line).ok_or_else(|| line.to_vec())?; EdHunk::Change(start, end, data.to_vec(), data2.to_vec()) } _ => return Err(line.to_vec()), }; hunks.push(hunk) } Ok(EdPatch { hunks }) } } #[cfg(test)] mod apply_patch_tests { use super::*; #[test] fn test_apply_add() { let patch = EdPatch { hunks: vec![EdHunk::Add(1, 1, b"hello\n".to_vec())], }; let data = &[&b"world\n"[..]][..]; assert_eq!(patch.apply(data).unwrap(), b"hello\nworld\n".to_vec()); } #[test] fn test_apply_remove() { let patch = EdPatch { hunks: vec![EdHunk::Remove(2, 2, b"world\n".to_vec())], }; let data = &[&b"hello\n"[..], &b"world\n"[..]]; assert_eq!(patch.apply(data).unwrap(), b"hello\n".to_vec()); } #[test] fn test_apply_change() { let patch = EdPatch { hunks: vec![EdHunk::Change( 2, 2, b"world\n".to_vec(), b"hello\n".to_vec(), )], }; let data = &[&b"hello\n"[..], &b"world\n"[..]]; assert_eq!(patch.apply(data).unwrap(), b"hello\nhello\n".to_vec()); } } #[cfg(test)] mod parse_patch_tests { use super::*; #[test] fn test_parse_patch() { let patch = b"5a10 > hello 5d10 < hello 5c10 < hello --- > hello "; let patch = EdPatch::parse_patch(patch).unwrap(); assert_eq!( patch, EdPatch { hunks: vec![ EdHunk::Add(5, 10, b"hello\n".to_vec()), EdHunk::Remove(5, 10, b"hello\n".to_vec()), EdHunk::Change(5, 10, b"hello\n".to_vec(), b"hello\n".to_vec()), ] } ); } } patchkit-0.2.1/src/lib.rs000064400000000000000000000052711046102023000133430ustar 00000000000000#![deny(missing_docs)] //! A crate for parsing and manipulating patches. //! //! # Examples //! //! ``` //! use patchkit::ContentPatch; //! use patchkit::unified::parse_patch; //! use patchkit::unified::{UnifiedPatch, Hunk, HunkLine}; //! //! let patch = UnifiedPatch::parse_patch(vec![ //! "--- a/file1\n", //! "+++ b/file1\n", //! "@@ -1,1 +1,1 @@\n", //! "-a\n", //! "+b\n", //! ].into_iter().map(|s| s.as_bytes())).unwrap(); //! //! assert_eq!(patch, UnifiedPatch { //! orig_name: b"a/file1".to_vec(), //! mod_name: b"b/file1".to_vec(), //! orig_ts: None, //! mod_ts: None, //! hunks: vec![ //! Hunk { //! mod_pos: 1, //! mod_range: 1, //! orig_pos: 1, //! orig_range: 1, //! lines: vec![ //! HunkLine::RemoveLine(b"a\n".to_vec()), //! HunkLine::InsertLine(b"b\n".to_vec()), //! ], //! tail: None //! }, //! ], //! }); //! //! let applied = patch.apply_exact(&b"a\n"[..]).unwrap(); //! assert_eq!(applied, b"b\n"); //! ``` pub mod ed; pub mod quilt; pub mod timestamp; pub mod unified; /// Strip the specified number of path components from the beginning of the path. pub fn strip_prefix(path: &std::path::Path, prefix: usize) -> &std::path::Path { let mut components = path.components(); for _ in 0..prefix { components.next(); } std::path::Path::new(components.as_path()) } /// Error that occurs when applying a patch #[derive(Debug)] pub enum ApplyError { /// A conflict occurred Conflict(String), /// The patch is unapplyable Unapplyable, } impl std::fmt::Display for ApplyError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Conflict(reason) => write!(f, "Conflict: {}", reason), Self::Unapplyable => write!(f, "Patch unapplyable"), } } } impl std::error::Error for ApplyError {} /// A patch to a single file pub trait SingleFilePatch: ContentPatch { /// Old file name fn oldname(&self) -> &[u8]; /// New file name fn newname(&self) -> &[u8]; } /// A patch that can be applied to file content pub trait ContentPatch { /// Apply this patch to a file fn apply_exact(&self, orig: &[u8]) -> Result, ApplyError>; } #[test] fn test_strip_prefix() { assert_eq!( std::path::PathBuf::from("b"), strip_prefix(std::path::Path::new("a/b"), 1) ); assert_eq!( std::path::PathBuf::from("a/b"), strip_prefix(std::path::Path::new("a/b"), 0) ); assert_eq!( std::path::PathBuf::from(""), strip_prefix(std::path::Path::new("a/b"), 2) ); } patchkit-0.2.1/src/quilt.rs000064400000000000000000000324131046102023000137310ustar 00000000000000//! Quilt patch management use std::collections::HashMap; use std::io::BufRead; /// The default directory for patches pub const DEFAULT_PATCHES_DIR: &str = "patches"; /// The default series file name pub const DEFAULT_SERIES_FILE: &str = "series"; /// Find the common prefix to use for patches /// /// # Arguments /// * `names` - An iterator of patch names /// /// # Returns /// The common prefix, or `None` if there is no common prefix pub fn find_common_patch_suffix<'a>(names: impl Iterator) -> Option<&'a str> { let mut suffix_count = HashMap::new(); for name in names { if name == "series" || name == "00list" { continue; } if name.starts_with("README") { continue; } let suffix = name.find('.').map(|index| &name[index..]).unwrap_or(""); suffix_count .entry(suffix) .and_modify(|count| *count += 1) .or_insert(1); } // Just find the suffix with the highest count and return it suffix_count .into_iter() .max_by_key(|(_, count)| *count) .map(|(suffix, _)| suffix) } #[cfg(test)] mod find_common_patch_suffix_tests { #[test] fn test_find_common_patch_suffix() { let names = vec![ "0001-foo.patch", "0002-bar.patch", "0003-baz.patch", "0004-qux.patch", ]; assert_eq!( super::find_common_patch_suffix(names.into_iter()), Some(".patch") ); } #[test] fn test_find_common_patch_suffix_no_common_suffix() { let names = vec![ "0001-foo.patch", "0002-bar.patch", "0003-baz.patch", "0004-qux", ]; assert_eq!( super::find_common_patch_suffix(names.into_iter()), Some(".patch") ); } #[test] fn test_find_common_patch_suffix_no_patches() { let names = vec![ "README", "0001-foo.patch", "0002-bar.patch", "0003-baz.patch", ]; assert_eq!( super::find_common_patch_suffix(names.into_iter()), Some(".patch") ); } } /// A entry in a series file #[derive(Debug, PartialEq, Eq)] pub enum SeriesEntry { /// A patch entry Patch { /// The name of the patch name: String, /// The options for patch options: Vec, }, /// A comment entry Comment(String), } /// A quilt series file #[derive(Debug)] pub struct Series { /// The entries in the series file pub entries: Vec, } impl Series { /// Create a new series file pub fn new() -> Self { Self { entries: vec![] } } /// Get the number of patches in the series file pub fn len(&self) -> usize { self.entries .iter() .filter(|entry| matches!(entry, SeriesEntry::Patch { .. })) .count() } /// Check if the series file is empty pub fn is_empty(&self) -> bool { self.len() == 0 } /// Check if the series file contains a patch pub fn contains(&self, name: &str) -> bool { self.entries.iter().any(|entry| match entry { SeriesEntry::Patch { name: entry_name, .. } => entry_name == name, _ => false, }) } /// Read a series file from a reader pub fn read(reader: R) -> std::io::Result { let mut series = Self::new(); let reader = std::io::BufReader::new(reader); for line in reader.lines() { let line = line?; let line = line.trim(); if line.starts_with('#') { series.entries.push(SeriesEntry::Comment(line.to_string())); continue; } let mut parts = line.split_whitespace(); let name = parts.next().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidData, "missing patch name in series file", ) })?; let options = parts.map(|s| s.to_string()).collect(); series.entries.push(SeriesEntry::Patch { name: name.to_string(), options, }); } Ok(series) } /// Remove a patch from the series file pub fn remove(&mut self, name: &str) { self.entries.retain(|entry| match entry { SeriesEntry::Patch { name: entry_name, .. } => entry_name != name, _ => true, }); } /// Get an iterator over the patch names in the series file pub fn patches(&self) -> impl Iterator { self.entries.iter().filter_map(|entry| match entry { SeriesEntry::Patch { name, .. } => Some(name.as_str()), _ => None, }) } /// Append a patch to the series file pub fn append(&mut self, name: &str, options: Option<&[String]>) { self.entries.push(SeriesEntry::Patch { name: name.to_string(), options: options.map(|options| options.to_vec()).unwrap_or_default(), }); } /// Write the series file to a writer pub fn write(&self, writer: &mut W) -> std::io::Result<()> { for entry in &self.entries { match entry { SeriesEntry::Patch { name, options } => { write!(writer, "{}", name)?; for option in options { write!(writer, " {}", option)?; } writeln!(writer)?; } SeriesEntry::Comment(comment) => { writeln!(writer, "# {}", comment)?; } } } Ok(()) } /// Get an iterator over the entries in the series file pub fn iter(&self) -> std::slice::Iter { self.entries.iter() } } impl std::ops::Index for Series { type Output = SeriesEntry; fn index(&self, index: usize) -> &Self::Output { &self.entries[index] } } impl Default for Series { fn default() -> Self { Self::new() } } /// Read a .pc/.quilt_patches file pub fn read_quilt_patches(mut reader: R) -> std::path::PathBuf { let mut p = String::new(); reader.read_to_string(&mut p).unwrap(); p.into() } /// Read a .pc/.quilt_series file pub fn read_quilt_series(mut reader: R) -> std::path::PathBuf { let mut s = String::new(); reader.read_to_string(&mut s).unwrap(); s.into() } /// A quilt patch pub struct QuiltPatch { /// The name of the patch pub name: String, /// The options for the patch pub options: Vec, /// The patch contents pub patch: Vec, } impl QuiltPatch { /// Get the patch contents as a byte slice pub fn as_bytes(&self) -> &[u8] { &self.patch } /// Get the name of the patch pub fn name(&self) -> &str { &self.name } /// Get the patch options pub fn options(&self) -> &[String] { &self.options } /// Get the patch contents pub fn parse(&self) -> Result, crate::unified::Error> { let lines = self.patch.split_inclusive(|&b| b == b'\n'); crate::unified::parse_patches(lines.map(|x| x.to_vec())) .filter_map(|patch| match patch { Ok(crate::unified::PlainOrBinaryPatch::Plain(patch)) => Some(Ok(patch)), Ok(crate::unified::PlainOrBinaryPatch::Binary(_)) => None, Err(err) => Some(Err(err)), }) .collect() } } /// Read quilt patches from a directory. pub fn iter_quilt_patches(directory: &std::path::Path) -> impl Iterator + '_ { let series_path = directory.join("series"); let series = if series_path.exists() { Series::read(std::fs::File::open(series_path).unwrap()).unwrap() } else { Series::new() }; series .iter() .filter_map(move |entry| { let (patch, options) = match entry { SeriesEntry::Patch { name, options } => (name, options), SeriesEntry::Comment(_) => return None, }; let p = directory.join(patch); let lines = std::fs::read_to_string(p).unwrap(); Some(QuiltPatch { name: patch.to_string(), patch: lines.into_bytes(), options: options.clone(), }) }) .collect::>() .into_iter() } #[cfg(test)] mod tests { use super::*; #[test] fn test_series_read() { let series = Series::read( r#"0001-foo.patch # This is a comment 0002-bar.patch --reverse 0003-baz.patch --reverse --fuzz=3 "# .as_bytes(), ) .unwrap(); assert_eq!(series.len(), 3); assert_eq!( series[0], SeriesEntry::Patch { name: "0001-foo.patch".to_string(), options: vec![] } ); assert_eq!( series[1], SeriesEntry::Comment("# This is a comment".to_string()) ); assert_eq!( series[2], SeriesEntry::Patch { name: "0002-bar.patch".to_string(), options: vec!["--reverse".to_string()] } ); assert_eq!( series[3], SeriesEntry::Patch { name: "0003-baz.patch".to_string(), options: vec!["--reverse".to_string(), "--fuzz=3".to_string()] } ); } #[test] fn test_series_write() { let mut series = Series::new(); series.append("0001-foo.patch", None); series.append("0002-bar.patch", Some(&["--reverse".to_string()])); series.append( "0003-baz.patch", Some(&["--reverse".to_string(), "--fuzz=3".to_string()]), ); let mut writer = vec![]; series.write(&mut writer).unwrap(); let series = String::from_utf8(writer).unwrap(); assert_eq!( series, "0001-foo.patch\n0002-bar.patch --reverse\n0003-baz.patch --reverse --fuzz=3\n" ); } #[test] fn test_series_remove() { let mut series = Series::new(); series.append("0001-foo.patch", None); series.append("0002-bar.patch", Some(&["--reverse".to_string()])); series.append( "0003-baz.patch", Some(&["--reverse".to_string(), "--fuzz=3".to_string()]), ); series.remove("0002-bar.patch"); let mut writer = vec![]; series.write(&mut writer).unwrap(); let series = String::from_utf8(writer).unwrap(); assert_eq!( series, "0001-foo.patch\n0003-baz.patch --reverse --fuzz=3\n" ); } #[test] fn test_series_contains() { let mut series = Series::new(); series.append("0001-foo.patch", None); series.append("0002-bar.patch", Some(&["--reverse".to_string()])); series.append( "0003-baz.patch", Some(&["--reverse".to_string(), "--fuzz=3".to_string()]), ); assert!(series.contains("0002-bar.patch")); assert!(!series.contains("0004-qux.patch")); } #[test] fn test_series_patches() { let mut series = Series::new(); series.append("0001-foo.patch", None); series.append("0002-bar.patch", Some(&["--reverse".to_string()])); series.append( "0003-baz.patch", Some(&["--reverse".to_string(), "--fuzz=3".to_string()]), ); let patches: Vec<_> = series.patches().collect(); assert_eq!( patches, &["0001-foo.patch", "0002-bar.patch", "0003-baz.patch"] ); } #[test] fn test_series_is_empty() { let series = Series::new(); assert!(series.is_empty()); let mut series = Series::new(); series.append("0001-foo.patch", None); assert!(!series.is_empty()); } #[test] fn test_quilt_patch_parse() { let patch = QuiltPatch { name: "0001-foo.patch".to_string(), options: vec![], patch: b"--- a/foo\n+++ b/foo\n@@ -1,3 +1,3 @@\n foo\n bar\n-bar\n+bar\n".to_vec(), }; let patches = patch.parse().unwrap(); assert_eq!(patches.len(), 1); assert_eq!( patches[0], crate::unified::UnifiedPatch { orig_name: b"a/foo".to_vec(), mod_name: b"b/foo".to_vec(), orig_ts: None, mod_ts: None, hunks: vec![crate::unified::Hunk { orig_pos: 1, orig_range: 3, mod_pos: 1, mod_range: 3, lines: vec![ crate::unified::HunkLine::ContextLine(b"foo\n".to_vec()), crate::unified::HunkLine::ContextLine(b"bar\n".to_vec()), crate::unified::HunkLine::RemoveLine(b"bar\n".to_vec()), crate::unified::HunkLine::InsertLine(b"bar\n".to_vec()) ], tail: None }] } ); } } patchkit-0.2.1/src/timestamp.rs000064400000000000000000000073451046102023000146040ustar 00000000000000//! Functions for parsing and formatting patch dates. use lazy_static::lazy_static; /// Error parsing a patch date. #[derive(Debug)] pub enum ParsePatchDateError { /// The date string is invalid. InvalidDate(String), /// The date string is missing a timezone offset. MissingTimezoneOffset(String), /// The timezone offset is invalid. InvalidTimezoneOffset(String), } /// Error formatting a patch date. #[derive(Debug)] pub enum FormatPatchDateError { /// The timezone offset is invalid. InvalidTimezoneOffset(i64), /// The time is negative. NegativeTime(i64, i64), } /// Format a patch date. pub fn format_patch_date(secs: i64, mut offset: i64) -> Result { if offset % 60 != 0 { return Err(FormatPatchDateError::InvalidTimezoneOffset(offset)); } // so that we don't need to do calculations on pre-epoch times, // which doesn't work with win32 python gmtime, we always // give the epoch in utc if secs == 0 { offset = 0; } if secs + offset < 0 { return Err(FormatPatchDateError::NegativeTime(secs, offset)); } let dt = chrono::DateTime::from_timestamp(secs, 0).unwrap(); let sign = if offset >= 0 { '+' } else { '-' }; let hours = offset.abs() / 3600; let minutes = (offset.abs() / 60) % 60; Ok(format!( "{} {}{:02}{:02}", dt.format("%Y-%m-%d %H:%M:%S"), sign, hours, minutes )) } /// Parse a patch date. pub fn parse_patch_date(date_str: &str) -> Result<(i64, i64), ParsePatchDateError> { lazy_static! { // Format for patch dates: %Y-%m-%d %H:%M:%S [+-]%H%M // Groups: 1 = %Y-%m-%d %H:%M:%S; 2 = [+-]%H; 3 = %M static ref RE_PATCHDATE: regex::Regex = regex::Regex::new(r"(\d+-\d+-\d+\s+\d+:\d+:\d+)\s*([+-]\d\d)(\d\d)$").unwrap(); static ref RE_PATCHDATE_NOOFFSET: regex:: Regex = regex::Regex::new(r"\d+-\d+-\d+\s+\d+:\d+:\d+$").unwrap(); } let m = RE_PATCHDATE.captures(date_str); if m.is_none() { if RE_PATCHDATE_NOOFFSET.captures(date_str).is_some() { return Err(ParsePatchDateError::MissingTimezoneOffset( date_str.to_string(), )); } else { return Err(ParsePatchDateError::InvalidDate(date_str.to_string())); } } let m = m.unwrap(); let secs_str = m.get(1).unwrap().as_str(); let offset_hours = m .get(2) .unwrap() .as_str() .parse::() .map_err(|_| ParsePatchDateError::InvalidTimezoneOffset(date_str.to_string()))?; let offset_minutes = m .get(3) .unwrap() .as_str() .parse::() .map_err(|_| ParsePatchDateError::InvalidTimezoneOffset(date_str.to_string()))?; if offset_hours.abs() >= 24 || offset_minutes >= 60 { return Err(ParsePatchDateError::InvalidTimezoneOffset( date_str.to_string(), )); } let offset = offset_hours * 3600 + offset_minutes * 60; // Parse secs_str with a time format %Y-%m-%d %H:%M:%S using the chrono crate let dt = chrono::NaiveDateTime::parse_from_str(secs_str, "%Y-%m-%d %H:%M:%S") .map_err(|_| ParsePatchDateError::InvalidDate(date_str.to_string()))? - chrono::Duration::seconds(offset); Ok((dt.and_utc().timestamp(), offset)) } #[cfg(test)] mod test { #[test] fn test_parse_patch_date() { assert_eq!( super::parse_patch_date("2019-01-01 00:00:00 +0000").unwrap(), (1546300800, 0) ); match super::parse_patch_date("2019-01-01 00:00:00") { Err(super::ParsePatchDateError::MissingTimezoneOffset(_)) => (), e => panic!("Expected MissingTimezoneOffset error, got {:?}", e), } } } patchkit-0.2.1/src/unified.rs000064400000000000000000001355511046102023000142250ustar 00000000000000//! Parsing of unified patches use crate::{ContentPatch, SingleFilePatch}; use regex::bytes::Regex; use std::num::ParseIntError; /// Errors that can occur while parsing a patch #[derive(Debug, PartialEq, Eq)] pub enum Error { /// The files are binary and differ BinaryFiles(Vec, Vec), /// A syntax error in the patch PatchSyntax(&'static str, Vec), /// A malformed patch header MalformedPatchHeader(&'static str, Vec), /// A malformed hunk header MalformedHunkHeader(String, Vec), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::BinaryFiles(oldname, newname) => { write!( f, "Binary files {} and {} differ", String::from_utf8_lossy(oldname), String::from_utf8_lossy(newname) ) } Self::PatchSyntax(msg, line) => write!(f, "Patch syntax error: {} in {:?}", msg, line), Self::MalformedPatchHeader(msg, line) => { write!( f, "Malformed patch header: {} in {}", msg, String::from_utf8_lossy(line) ) } Self::MalformedHunkHeader(msg, line) => { write!( f, "Malformed hunk header: {} in {}", msg, String::from_utf8_lossy(line) ) } } } } impl std::error::Error for Error {} /// Split lines but preserve trailing newlines pub fn splitlines(data: &[u8]) -> impl Iterator { let mut start = 0; let mut end = 0; std::iter::from_fn(move || loop { if end == data.len() { if start == end { return None; } let line = &data[start..end]; start = end; return Some(line); } let c = data[end]; end += 1; if c == b'\n' { let line = &data[start..end]; start = end; return Some(line); } }) } #[cfg(test)] mod splitlines_tests { #[test] fn test_simple() { let data = b"line 1\nline 2\nline 3\n"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!( lines, vec![ "line 1\n".as_bytes(), "line 2\n".as_bytes(), "line 3\n".as_bytes() ] ); } #[test] fn test_no_trailing() { let data = b"line 1\nline 2\nline 3"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!( lines, vec![&b"line 1\n"[..], &b"line 2\n"[..], &b"line 3"[..]] ); } #[test] fn test_empty_line() { let data = b"line 1\n\nline 3\n"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!(lines, vec![&b"line 1\n"[..], &b"\n"[..], &b"line 3\n"[..]]); } } /// The string that indicates that a line has no newline pub const NO_NL: &[u8] = b"\\ No newline at end of file\n"; /// Iterate through a series of lines, ensuring that lines /// that originally had no terminating newline are produced /// without one. pub fn iter_lines_handle_nl<'a, I>(mut iter_lines: I) -> impl Iterator + 'a where I: Iterator + 'a, { let mut last_line: Option<&'a [u8]> = None; std::iter::from_fn(move || { for line in iter_lines.by_ref() { if line == NO_NL { if let Some(last) = last_line.as_mut() { assert!(last.ends_with(b"\n")); // Drop the last newline from `last` *last = &last[..last.len() - 1]; } else { panic!("No newline indicator without previous line"); } } else { if let Some(last) = last_line.take() { last_line = Some(line); return Some(last); } last_line = Some(line); } } last_line.take() }) } #[test] fn test_iter_lines_handle_nl() { let lines = vec![ &b"line 1\n"[..], &b"line 2\n"[..], &b"line 3\n"[..], &b"line 4\n"[..], &b"\\ No newline at end of file\n"[..], ]; let mut iter = iter_lines_handle_nl(lines.into_iter()); assert_eq!(iter.next(), Some("line 1\n".as_bytes())); assert_eq!(iter.next(), Some("line 2\n".as_bytes())); assert_eq!(iter.next(), Some("line 3\n".as_bytes())); assert_eq!(iter.next(), Some("line 4".as_bytes())); assert_eq!(iter.next(), None); } static BINARY_FILES_RE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { lazy_regex::BytesRegex::new(r"^Binary files (.+) and (.+) differ").unwrap() }); /// Get the names of the files in a patch pub fn get_patch_names<'a, T: Iterator>( iter_lines: &mut T, ) -> Result<((Vec, Option>), (Vec, Option>)), Error> { let line = iter_lines .next() .ok_or_else(|| Error::PatchSyntax("No input", vec![]))?; if let Some(captures) = BINARY_FILES_RE.captures(line) { let orig_name = captures.get(1).unwrap().as_bytes().to_vec(); let mod_name = captures.get(2).unwrap().as_bytes().to_vec(); return Err(Error::BinaryFiles(orig_name, mod_name)); } let orig_name = line .strip_prefix(b"--- ") .ok_or_else(|| Error::MalformedPatchHeader("No orig name", line.to_vec()))? .strip_suffix(b"\n") .ok_or_else(|| Error::PatchSyntax("missing newline", line.to_vec()))?; let (orig_name, orig_ts) = match orig_name.split(|&c| c == b'\t').collect::>()[..] { [name, ts] => (name.to_vec(), Some(ts.to_vec())), [name] => (name.to_vec(), None), _ => return Err(Error::MalformedPatchHeader("No orig line", line.to_vec())), }; let line = iter_lines .next() .ok_or_else(|| Error::PatchSyntax("No input", vec![]))?; let (mod_name, mod_ts) = match line.strip_prefix(b"+++ ") { Some(line) => { let mod_name = line .strip_suffix(b"\n") .ok_or_else(|| Error::PatchSyntax("missing newline", line.to_vec()))?; let (mod_name, mod_ts) = match mod_name.split(|&c| c == b'\t').collect::>()[..] { [name, ts] => (name.to_vec(), Some(ts.to_vec())), [name] => (name.to_vec(), None), _ => return Err(Error::PatchSyntax("Invalid mod name", line.to_vec())), }; (mod_name, mod_ts) } None => return Err(Error::MalformedPatchHeader("No mod line", line.to_vec())), }; Ok(((orig_name, orig_ts), (mod_name, mod_ts))) } #[cfg(test)] mod get_patch_names_tests { #[test] fn test_simple() { let lines = [ &b"--- baz 2009-10-14 19:49:59 +0000\n"[..], &b"+++ quxx 2009-10-14 19:51:00 +0000\n"[..], ]; let mut iter = lines.into_iter(); let (old, new) = super::get_patch_names(&mut iter).unwrap(); assert_eq!( old, (b"baz".to_vec(), Some(b"2009-10-14 19:49:59 +0000".to_vec())) ); assert_eq!( new, ( b"quxx".to_vec(), Some(b"2009-10-14 19:51:00 +0000".to_vec()) ) ); } #[test] fn test_binary() { let lines = [&b"Binary files qoo and bar differ\n"[..]]; let mut iter = lines.into_iter(); let e = super::get_patch_names(&mut iter).unwrap_err(); assert_eq!( e, super::Error::BinaryFiles(b"qoo".to_vec(), b"bar".to_vec()) ); } } /// Iterate over the hunks in a patch /// /// # Arguments /// * `iter_lines`: Iterator over lines pub fn iter_hunks<'a, I>(iter_lines: &mut I) -> impl Iterator> + '_ where I: Iterator, { std::iter::from_fn(move || { while let Some(line) = iter_lines.next() { if line == b"\n" { continue; } match Hunk::from_header(line) { Ok(mut new_hunk) => { let mut orig_size = 0; let mut mod_size = 0; while orig_size < new_hunk.orig_range || mod_size < new_hunk.mod_range { let line = iter_lines.next()?; match HunkLine::parse_line(line) { Err(_) => { return Some(Err(Error::PatchSyntax( "Invalid hunk line", line.to_vec(), ))); } Ok(hunk_line) => { if matches!( hunk_line, HunkLine::RemoveLine(_) | HunkLine::ContextLine(_) ) { orig_size += 1 } if matches!( hunk_line, HunkLine::InsertLine(_) | HunkLine::ContextLine(_) ) { mod_size += 1 } new_hunk.lines.push(hunk_line); } } } return Some(Ok(new_hunk)); } Err(MalformedHunkHeader(m, l)) => { return Some(Err(Error::MalformedHunkHeader(m.to_string(), l))); } } } None }) } #[cfg(test)] mod iter_hunks_tests { use super::*; #[test] fn test_iter_hunks() { let mut lines = super::splitlines( br#"@@ -391,6 +391,8 @@ else: assert isinstance(hunk_line, RemoveLine) line_no += 1 + for line in orig_lines: + yield line import unittest import os.path "#, ); let hunks = super::iter_hunks(&mut lines) .collect::, Error>>() .unwrap(); let mut expected_hunk = Hunk::new(391, 6, 391, 8, None); expected_hunk.lines.extend([ HunkLine::ContextLine(b" else:\n".to_vec()), HunkLine::ContextLine( b" assert isinstance(hunk_line, RemoveLine)\n".to_vec(), ), HunkLine::ContextLine(b" line_no += 1\n".to_vec()), HunkLine::InsertLine(b" for line in orig_lines:\n".to_vec()), HunkLine::InsertLine(b" yield line\n".to_vec()), HunkLine::ContextLine(b" \n".to_vec()), HunkLine::ContextLine(b"import unittest\n".to_vec()), HunkLine::ContextLine(b"import os.path\n".to_vec()), ]); assert_eq!(&expected_hunk, hunks.first().unwrap()); } } /// Parse a patch file /// /// # Arguments /// * `iter_lines`: Iterator over lines pub fn parse_patch<'a, I>(iter_lines: I) -> Result where I: Iterator + 'a, { let mut iter_lines = iter_lines_handle_nl(iter_lines); let ((orig_name, orig_ts), (mod_name, mod_ts)) = match get_patch_names(&mut iter_lines) { Ok(names) => names, Err(Error::BinaryFiles(orig_name, mod_name)) => { return Ok(PlainOrBinaryPatch::Binary(BinaryPatch(orig_name, mod_name))); } Err(e) => return Err(e), }; let mut patch = UnifiedPatch::new(orig_name, orig_ts, mod_name, mod_ts); for hunk in iter_hunks(&mut iter_lines) { patch.hunks.push(hunk?); } Ok(PlainOrBinaryPatch::Plain(patch)) } #[cfg(test)] mod patches_tests { use super::*; macro_rules! test_patch { ($name:ident, $orig:expr, $mod:expr, $patch:expr) => { #[test] fn $name() { let orig = include_bytes!(concat!("../test_patches_data/", $orig)); let modi = include_bytes!(concat!("../test_patches_data/", $mod)); let patch = include_bytes!(concat!("../test_patches_data/", $patch)); let parsed = super::parse_patch(super::splitlines(patch)).unwrap(); let mut patched = Vec::new(); let mut iter = parsed.apply_exact(orig).unwrap().into_iter(); while let Some(line) = iter.next() { patched.push(line); } assert_eq!(patched, modi); } }; } test_patch!(test_patch_2, "orig-2", "mod-2", "diff-2"); test_patch!(test_patch_3, "orig-3", "mod-3", "diff-3"); test_patch!(test_patch_4, "orig-4", "mod-4", "diff-4"); test_patch!(test_patch_5, "orig-5", "mod-5", "diff-5"); test_patch!(test_patch_6, "orig-6", "mod-6", "diff-6"); test_patch!(test_patch_7, "orig-7", "mod-7", "diff-7"); } /// Conflict applying a patch #[derive(Debug)] pub struct PatchConflict { line_no: usize, orig_line: Vec, patch_line: Vec, } impl std::fmt::Display for PatchConflict { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "Patch conflict at orig line {}: orig: {:?}, patch: {:?}", self.line_no, String::from_utf8_lossy(&self.orig_line), String::from_utf8_lossy(&self.patch_line) ) } } impl std::error::Error for PatchConflict {} struct PatchedIter, L: Iterator>> { orig_lines: L, hunk_lines: Vec, hunks: std::iter::Peekable, line_no: usize, } impl, L: Iterator>> Iterator for PatchedIter { type Item = Result, PatchConflict>; fn next(&mut self) -> Option, PatchConflict>> { loop { // First, check if we just need to yield the next line from the original file. match self.hunks.peek_mut() { // We're ahead of the next hunk. Yield the next line from the original file. Some(hunk) if self.line_no < hunk.orig_pos => { self.line_no += 1; if let Some(line) = self.orig_lines.next() { return Some(Ok(line)); } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: Vec::new(), })); } } // There are no more hunks. Yield the rest of the original file. None => { if let Some(line) = self.orig_lines.next() { return Some(Ok(line)); } else { return None; } } Some(_hunk) => { // We're in a hunk. Check if we need to yield a line from the hunk. if let Some(line) = self.hunk_lines.pop() { match line { HunkLine::ContextLine(bytes) => { if let Some(orig_line) = self.orig_lines.next() { if orig_line != bytes { return Some(Err(PatchConflict { line_no: self.line_no, orig_line, patch_line: bytes, })); } } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: bytes, })); } self.line_no += 1; return Some(Ok(bytes)); } HunkLine::InsertLine(bytes) => { return Some(Ok(bytes)); } HunkLine::RemoveLine(bytes) => { if let Some(orig_line) = self.orig_lines.next() { if orig_line != bytes { return Some(Err(PatchConflict { line_no: self.line_no, orig_line, patch_line: bytes, })); } } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: bytes, })); } self.line_no += 1; } } } else { self.hunks.next(); if let Some(h) = self.hunks.peek_mut() { let mut hunk_lines = h.lines.drain(..).collect::>(); hunk_lines.reverse(); self.hunk_lines = hunk_lines; } } } } } } } #[cfg(test)] mod iter_exact_patched_from_hunks_tests { use super::*; #[test] fn test_just_context() { let orig_lines = vec![ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ]; let mut hunk = Hunk::new(1, 1, 1, 1, None); hunk.lines.push(HunkLine::ContextLine(b"line 1\n".to_vec())); let hunks = vec![hunk]; let result = super::iter_exact_patched_from_hunks(orig_lines.into_iter(), hunks.into_iter()) .collect::, _>>() .unwrap(); assert_eq!( &result, &[ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ] ); } #[test] fn test_insert() { let orig_lines = vec![ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ]; let mut hunk = Hunk::new(1, 0, 1, 1, None); hunk.lines.push(HunkLine::InsertLine(b"line 0\n".to_vec())); hunk.lines.push(HunkLine::ContextLine(b"line 1\n".to_vec())); let hunks = vec![hunk]; let result = super::iter_exact_patched_from_hunks(orig_lines.into_iter(), hunks.into_iter()) .collect::, _>>() .unwrap(); assert_eq!( &result, &[ b"line 0\n".to_vec(), b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ] ); } } /// Iterate through a series of lines with a patch applied. /// /// This handles a single file, and does exact, not fuzzy patching. /// /// Args: /// orig_lines: The original lines of the file. /// hunks: The hunks to apply to the file. pub fn iter_exact_patched_from_hunks<'a>( orig_lines: impl Iterator> + 'a, hunks: impl Iterator, ) -> impl Iterator, PatchConflict>> { let mut hunks = hunks.peekable(); let mut hunk_lines = if let Some(h) = hunks.peek_mut() { h.lines.drain(..).collect() } else { Vec::new() }; hunk_lines.reverse(); PatchedIter { orig_lines, hunks, line_no: 1, hunk_lines, } } /// Find the index of the first character that differs between two texts pub fn difference_index(atext: &[u8], btext: &[u8]) -> Option { let length = atext.len().min(btext.len()); (0..length).find(|&i| atext[i] != btext[i]) } /// Parse a patch file #[derive(PartialEq, Eq)] pub enum FileEntry { /// Non-patch data Junk(Vec>), /// A meta entry Meta(Vec), /// A patch entry Patch(Vec>), } impl std::fmt::Debug for FileEntry { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Junk(lines) => { write!(f, "Junk[")?; // Print the lines interspersed with commas for (i, line) in lines.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{:?}", String::from_utf8_lossy(line))?; } write!(f, "]")?; Ok(()) } Self::Meta(line) => write!(f, "Meta({:?})", String::from_utf8_lossy(line)), Self::Patch(lines) => { write!(f, "Patch[")?; // Print the lines interspersed with commas for (i, line) in lines.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{:?}", String::from_utf8_lossy(line))?; } write!(f, "]")?; Ok(()) } } } } struct FileEntryIter { iter: I, saved_lines: Vec>, is_dirty: bool, orig_range: usize, mod_range: usize, } impl FileEntryIter where I: Iterator>, { fn entry(&mut self) -> Option { if !self.saved_lines.is_empty() { let lines = self.saved_lines.drain(..).collect(); if self.is_dirty { Some(FileEntry::Junk(lines)) } else { Some(FileEntry::Patch(lines)) } } else { None } } } impl Iterator for FileEntryIter where I: Iterator>, { type Item = Result; fn next(&mut self) -> Option> { loop { let line = match self.iter.next() { Some(line) => line, None => { if let Some(entry) = self.entry() { return Some(Ok(entry)); } else { return None; } } }; if line.starts_with(b"=== ") { return Some(Ok(FileEntry::Meta(line))); } else if line.starts_with(b"*** ") { continue; } else if line.starts_with(b"#") { continue; } else if self.orig_range > 0 || self.mod_range > 0 { if line.starts_with(b"-") || line.starts_with(b" ") { self.orig_range -= 1; } if line.starts_with(b"+") || line.starts_with(b" ") { self.mod_range -= 1; } self.saved_lines.push(line); } else if line.starts_with(b"--- ") || BINARY_FILES_RE.is_match(line.as_slice()) { let entry = self.entry(); self.is_dirty = false; self.saved_lines.push(line); if let Some(entry) = entry { return Some(Ok(entry)); } } else if line.starts_with(b"+++ ") && !self.is_dirty { self.saved_lines.push(line); } else if line.starts_with(b"@@") { let hunk = match Hunk::from_header(line.as_slice()) { Ok(hunk) => hunk, Err(e) => { return Some(Err(Error::MalformedHunkHeader(e.to_string(), line.clone()))); } }; self.orig_range = hunk.orig_range; self.mod_range = hunk.mod_range; self.saved_lines.push(line); } else { let entry = if !self.is_dirty { self.entry() } else { None }; self.saved_lines.push(line); self.is_dirty = true; if let Some(entry) = entry { return Some(Ok(entry)); } } } } } /// Iterate through a series of lines. /// /// # Arguments /// * `orig` - The original lines of the file. pub fn iter_file_patch(orig: I) -> impl Iterator> where I: Iterator>, { FileEntryIter { iter: orig, orig_range: 0, saved_lines: Vec::new(), is_dirty: false, mod_range: 0, } } #[cfg(test)] mod iter_file_patch_tests { #[test] fn test_simple() { let lines = [ "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![super::FileEntry::Patch( lines .iter() .map(|l| l.as_bytes().to_vec()) .collect::>() )] ); } #[test] fn test_noise() { let lines = [ "=== modified file 'test.txt'\n", "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![ super::FileEntry::Meta(lines[0].as_bytes().to_vec()), super::FileEntry::Patch( lines .iter() .skip(1) .map(|l| l.as_bytes().to_vec()) .collect::>() ) ] ); } #[test] fn test_allow_dirty() { let lines = [ "Foo bar\n", "Bar blah\n", "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![ super::FileEntry::Junk( lines .iter() .take(2) .map(|l| l.as_bytes().to_vec()) .collect::>() ), super::FileEntry::Patch( lines .iter() .skip(2) .map(|l| l.as_bytes().to_vec()) .collect::>() ) ] ); } } /// A patch that can be applied to a single file pub enum PlainOrBinaryPatch { /// A unified patch Plain(UnifiedPatch), /// An indication that two binary files differ Binary(BinaryPatch), } impl SingleFilePatch for PlainOrBinaryPatch { fn oldname(&self) -> &[u8] { match self { Self::Plain(patch) => patch.orig_name.as_slice(), Self::Binary(patch) => patch.0.as_slice(), } } fn newname(&self) -> &[u8] { match self { Self::Plain(patch) => patch.mod_name.as_slice(), Self::Binary(patch) => patch.1.as_slice(), } } } impl crate::ContentPatch for PlainOrBinaryPatch { fn apply_exact(&self, orig: &[u8]) -> Result, crate::ApplyError> { match self { Self::Plain(patch) => patch.apply_exact(orig), Self::Binary(_) => Err(crate::ApplyError::Unapplyable), } } } /// Parse a patch file /// /// # Arguments /// * `iter`: Iterator over lines pub fn parse_patches(iter: I) -> impl Iterator> where I: Iterator>, { iter_file_patch(iter).filter_map(|entry| match entry { Ok(FileEntry::Patch(lines)) => match parse_patch(lines.iter().map(|l| l.as_slice())) { Ok(patch) => Some(Ok(patch)), Err(e) => Some(Err(e)), }, Ok(FileEntry::Junk(_)) => None, Ok(FileEntry::Meta(_)) => None, Err(e) => Some(Err(e)), }) } #[cfg(test)] mod parse_patches_tests { #[test] fn test_simple() { let lines = [ "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let patches = super::parse_patches(lines.iter().map(|l| l.as_bytes().to_vec())).collect::>(); assert_eq!(patches.len(), 1); } } /// A binary patch #[derive(Clone, Debug, PartialEq, Eq)] pub struct BinaryPatch(pub Vec, pub Vec); impl SingleFilePatch for BinaryPatch { fn oldname(&self) -> &[u8] { &self.0 } fn newname(&self) -> &[u8] { &self.1 } } impl crate::ContentPatch for BinaryPatch { fn apply_exact(&self, _orig: &[u8]) -> Result, crate::ApplyError> { Err(crate::ApplyError::Unapplyable) } } /// A unified diff style patch #[derive(Clone, Debug, PartialEq, Eq)] pub struct UnifiedPatch { /// Name of the original file pub orig_name: Vec, /// Timestamp for the original file pub orig_ts: Option>, /// Name of the modified file pub mod_name: Vec, /// Timestamp for the modified file pub mod_ts: Option>, /// List of hunks pub hunks: Vec, } impl UnifiedPatch { /// Create a new patch pub fn new( orig_name: Vec, orig_ts: Option>, mod_name: Vec, mod_ts: Option>, ) -> Self { Self { orig_name, orig_ts, mod_name, mod_ts, hunks: Vec::new(), } } /// Serialize this patch to a byte vector pub fn as_bytes(&self) -> Vec { let mut bytes = Vec::new(); self.write(&mut bytes).unwrap(); bytes } /// Write this patch to a writer pub fn write(&self, w: &mut W) -> std::io::Result<()> { w.write_all( &format!( "--- {}{}\n", String::from_utf8_lossy(&self.orig_name), match &self.orig_ts { Some(ts) => format!("\t{}", String::from_utf8_lossy(ts)), None => "".to_string(), } ) .into_bytes(), )?; w.write_all( &format!( "+++ {}{}\n", String::from_utf8_lossy(&self.mod_name), match &self.mod_ts { Some(ts) => format!("\t{}", String::from_utf8_lossy(ts)), None => "".to_string(), } ) .into_bytes(), )?; for hunk in &self.hunks { hunk.write(w)?; } Ok(()) } /// Parse a unified patch file /// /// # Arguments /// * `iter_lines`: Iterator over lines pub fn parse_patch<'a, I>(iter_lines: I) -> Result where I: Iterator + 'a, { let mut iter_lines = iter_lines_handle_nl(iter_lines); let ((orig_name, orig_ts), (mod_name, mod_ts)) = match get_patch_names(&mut iter_lines) { Ok(names) => names, Err(e) => return Err(e), }; let mut patch = Self::new(orig_name, orig_ts, mod_name, mod_ts); for hunk in iter_hunks(&mut iter_lines) { patch.hunks.push(hunk?); } Ok(patch) } /// Parse a unified patch file /// /// # Arguments /// * `iter`: Iterator over lines pub fn parse_patches(iter: I) -> Result, Error> where I: Iterator>, { iter_file_patch(iter) .filter_map(|entry| match entry { Ok(FileEntry::Patch(lines)) => { match Self::parse_patch(lines.iter().map(|l| l.as_slice())) { Ok(patch) => Some(Ok(PlainOrBinaryPatch::Plain(patch))), Err(e) => Some(Err(e)), } } Ok(FileEntry::Junk(_)) => None, Ok(FileEntry::Meta(_)) => None, Err(Error::BinaryFiles(orig_name, mod_name)) => Some(Ok( PlainOrBinaryPatch::Binary(BinaryPatch(orig_name, mod_name)), )), Err(e) => Some(Err(e)), }) .collect() } } impl SingleFilePatch for UnifiedPatch { /// Old file name fn oldname(&self) -> &[u8] { &self.orig_name } /// New file name fn newname(&self) -> &[u8] { &self.mod_name } } impl ContentPatch for UnifiedPatch { /// Apply this patch to a file fn apply_exact(&self, orig: &[u8]) -> Result, crate::ApplyError> { let orig_lines = splitlines(orig).map(|l| l.to_vec()); let lines = iter_exact_patched_from_hunks(orig_lines, self.hunks.clone().into_iter()) .collect::>, PatchConflict>>() .map_err(|e| crate::ApplyError::Conflict(e.to_string()))?; Ok(lines.concat()) } } #[cfg(test)] mod patch_tests { #[test] fn test_as_bytes_empty_hunks() { let patch = super::UnifiedPatch { orig_name: b"foo".to_vec(), orig_ts: None, mod_name: b"bar".to_vec(), mod_ts: None, hunks: vec![], }; assert_eq!(patch.as_bytes(), b"--- foo\n+++ bar\n"); } #[test] fn test_as_bytes() { let patch = super::UnifiedPatch { orig_name: b"foo".to_vec(), orig_ts: None, mod_name: b"bar".to_vec(), mod_ts: None, hunks: vec![super::Hunk { orig_pos: 1, orig_range: 1, mod_pos: 2, mod_range: 1, tail: None, lines: vec![super::HunkLine::ContextLine(b"foo\n".to_vec())], }], }; assert_eq!(patch.as_bytes(), b"--- foo\n+++ bar\n@@ -1 +2 @@\n foo\n"); } } /// A line in a hunk #[derive(Clone, Debug, PartialEq, Eq)] pub enum HunkLine { /// A line that is unchanged ContextLine(Vec), /// A line that was inserted InsertLine(Vec), /// A line that was removed RemoveLine(Vec), } impl HunkLine { /// Get the character that represents this hunk line pub fn char(&self) -> u8 { match self { Self::ContextLine(_) => b' ', Self::InsertLine(_) => b'+', Self::RemoveLine(_) => b'-', } } /// Get the contents of this hunk line pub fn contents(&self) -> &[u8] { match self { Self::ContextLine(bytes) => bytes, Self::InsertLine(bytes) => bytes, Self::RemoveLine(bytes) => bytes, } } /// Serialize this hunk line to a byte vector pub fn as_bytes(&self) -> Vec { let leadchar = self.char(); let contents = self.contents(); let terminator = if !contents.ends_with(&b"\n"[..]) { [b"\n".to_vec(), NO_NL.to_vec()].concat() } else { b"".to_vec() }; [vec![leadchar], contents.to_vec(), terminator].concat() } /// Parse a hunk line pub fn parse_line(line: &[u8]) -> Result { if line.starts_with(b"\n") { Ok(Self::ContextLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b" ") { Ok(Self::ContextLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b"+") { Ok(Self::InsertLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b"-") { Ok(Self::RemoveLine(line.to_vec())) } else { Err(MalformedLine(line.to_vec())) } } } /// An error that occurs when parsing a hunk line #[derive(Clone, Debug, PartialEq, Eq)] pub struct MalformedLine(Vec); impl std::fmt::Display for MalformedLine { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Malformed line: {:?}", self.0) } } impl std::error::Error for MalformedLine {} #[cfg(test)] mod hunkline_tests { use super::HunkLine; use super::MalformedLine; #[test] fn test_parse_line() { assert_eq!( HunkLine::parse_line(&b" foo\n"[..]).unwrap(), HunkLine::ContextLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"-foo\n"[..]).unwrap(), HunkLine::RemoveLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"+foo\n"[..]).unwrap(), HunkLine::InsertLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"\n"[..]).unwrap(), HunkLine::ContextLine(b"\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"aaaaa\n"[..]).unwrap_err(), MalformedLine(b"aaaaa\n".to_vec()) ); } #[test] fn as_bytes() { assert_eq!( HunkLine::ContextLine(b"foo\n".to_vec()).as_bytes(), b" foo\n" ); assert_eq!( HunkLine::InsertLine(b"foo\n".to_vec()).as_bytes(), b"+foo\n" ); assert_eq!( HunkLine::RemoveLine(b"foo\n".to_vec()).as_bytes(), b"-foo\n" ); } #[test] fn as_bytes_no_nl() { assert_eq!( HunkLine::ContextLine(b"foo".to_vec()).as_bytes(), b" foo\n\\ No newline at end of file\n" ); } } /// An error that occurs when parsing a hunk header #[derive(Clone, Debug, PartialEq, Eq)] pub struct MalformedHunkHeader(pub &'static str, pub Vec); impl std::fmt::Display for MalformedHunkHeader { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "Malformed hunk header: {}: {}", self.0, String::from_utf8_lossy(&self.1) ) } } impl std::error::Error for MalformedHunkHeader {} /// A hunk in a patch #[derive(PartialEq, Eq, Debug, Clone)] pub struct Hunk { /// Position in the original file pub orig_pos: usize, /// Number of lines in the original file pub orig_range: usize, /// Position in the modified file pub mod_pos: usize, /// Number of lines in the modified file pub mod_range: usize, /// Tail of the hunk header pub tail: Option>, /// Lines in the hunk pub lines: Vec, } impl Hunk { /// Create a new hunk pub fn new( orig_pos: usize, orig_range: usize, mod_pos: usize, mod_range: usize, tail: Option>, ) -> Self { Self { orig_pos, orig_range, mod_pos, mod_range, tail, lines: Vec::new(), } } /// Parse a hunk header pub fn from_header(line: &[u8]) -> Result { let re = lazy_regex::regex!(r"\@\@ ([^@]*) \@\@( (.*))?\n"B); let captures = re .captures(line) .ok_or_else(|| MalformedHunkHeader("Does not match format.", line.to_vec()))?; let (orig, modi) = match captures[1].split(|b| *b == b' ').collect::>()[..] { [orig, modi] => Ok((orig, modi)), _ => return Err(MalformedHunkHeader("Does not match format.", line.to_vec())), }?; if orig[0] != b'-' || modi[0] != b'+' { return Err(MalformedHunkHeader( "Positions don't start with + or -.", line.to_vec(), )); } let (orig_pos, orig_range) = parse_range(&String::from_utf8_lossy(&orig[1..])) .map_err(|_| MalformedHunkHeader("Original range is not a number.", line.to_vec()))?; let (mod_pos, mod_range) = parse_range(&String::from_utf8_lossy(modi[1..].as_ref())) .map_err(|_| MalformedHunkHeader("Modified range is not a number.", line.to_vec()))?; let tail = captures.get(3).map(|m| m.as_bytes().to_vec()); Ok(Self::new(orig_pos, orig_range, mod_pos, mod_range, tail)) } /// Get the lines in this hunk pub fn lines(&self) -> &[HunkLine] { &self.lines } /// Get the header of this hunk pub fn get_header(&self) -> Vec { let tail_str = match &self.tail { Some(tail) => [b" ".to_vec(), tail.to_vec()].concat(), None => Vec::new(), }; format!( "@@ -{} +{} @@{}\n", self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range), String::from_utf8_lossy(&tail_str), ) .into_bytes() } fn range_str(&self, pos: usize, range: usize) -> String { if range == 1 { format!("{}", pos) } else { format!("{},{}", pos, range) } } /// Write this hunk to a writer pub fn write(&self, w: &mut W) -> std::io::Result<()> { w.write_all(&self.get_header())?; for line in &self.lines { w.write_all(&line.as_bytes())?; } Ok(()) } /// Serialize this hunk to a byte vector pub fn as_bytes(&self) -> Vec { let mut bytes = Vec::new(); self.write(&mut bytes).unwrap(); bytes } /// Shift a position to the modified file pub fn shift_to_mod(&self, pos: usize) -> Option { if pos < self.orig_pos - 1 { Some(0) } else if pos > self.orig_pos + self.orig_range { Some((self.mod_range as isize) - (self.orig_range as isize)) } else { self.shift_to_mod_lines(pos) } } /// Shift a position to the original file fn shift_to_mod_lines(&self, pos: usize) -> Option { let mut position = self.orig_pos - 1; let mut shift = 0; for line in &self.lines { match line { HunkLine::InsertLine(_) => shift += 1, HunkLine::RemoveLine(_) => { if position == pos { return None; } shift -= 1; position += 1; } HunkLine::ContextLine(_) => position += 1, } if position > pos { break; } } Some(shift) } } /// Parse a patch range, handling the "1" special-case pub fn parse_range(textrange: &str) -> Result<(usize, usize), ParseIntError> { let tmp: Vec<&str> = textrange.split(',').collect(); let (pos, brange) = if tmp.len() == 1 { (tmp[0], "1") } else { (tmp[0], tmp[1]) }; let pos = pos.parse::()?; let range = brange.parse::()?; Ok((pos, range)) } #[cfg(test)] mod hunk_tests { use super::Hunk; #[test] fn from_header_test() { let hunk = Hunk::from_header(&b"@@ -1 +2 @@\n"[..]).unwrap(); assert_eq!(hunk, Hunk::new(1, 1, 2, 1, None)); } #[test] fn from_header_tail() { let hunk = Hunk::from_header(&b"@@ -1 +2 @@ function()\n"[..]).unwrap(); assert_eq!(hunk, Hunk::new(1, 1, 2, 1, Some(b"function()".to_vec()))); } #[test] fn test_valid_hunk_header() { let header = b"@@ -34,11 +50,6 @@\n"; let hunk = Hunk::from_header(&header[..]).unwrap(); assert_eq!(hunk.orig_pos, 34); assert_eq!(hunk.orig_range, 11); assert_eq!(hunk.mod_pos, 50); assert_eq!(hunk.mod_range, 6); assert_eq!(hunk.as_bytes(), &header[..]); } #[test] fn test_valid_hunk_header2() { let header = b"@@ -1 +0,0 @@\n"; let hunk = Hunk::from_header(&header[..]).unwrap(); assert_eq!(hunk.orig_pos, 1); assert_eq!(hunk.orig_range, 1); assert_eq!(hunk.mod_pos, 0); assert_eq!(hunk.mod_range, 0); assert_eq!(hunk.as_bytes(), header); } /// Parse a hunk header produced by diff -p. #[test] fn test_pdiff() { let header = b"@@ -407,7 +292,7 @@ bzr 0.18rc1 2007-07-10\n"; let hunk = Hunk::from_header(header).unwrap(); assert_eq!(&b"bzr 0.18rc1 2007-07-10"[..], hunk.tail.as_ref().unwrap()); assert_eq!(&header[..], hunk.as_bytes()); } fn assert_malformed_header(header: &[u8]) { let err = Hunk::from_header(header).unwrap_err(); assert!(matches!(err, super::MalformedHunkHeader(..))); } #[test] fn test_invalid_header() { assert_malformed_header(&b" -34,11 +50,6 \n"[..]); assert_malformed_header(&b"@@ +50,6 -34,11 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,6 @@"[..]); assert_malformed_header(&b"@@ -34.5,11 +50,6 @@\n"[..]); assert_malformed_header(&b"@@-34,11 +50,6@@\n"[..]); assert_malformed_header(&b"@@ 34,11 50,6 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,6.5 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,-6 @@\n"[..]); } } #[cfg(test)] mod parse_range_tests { use super::parse_range; #[test] fn parse_range_test() { assert_eq!((2, 1), parse_range("2").unwrap()); assert_eq!((2, 1), parse_range("2,1").unwrap()); parse_range("foo").unwrap_err(); } } patchkit-0.2.1/test_patches_data/binary-after-normal.patch000064400000000000000000000002651046102023000220070ustar 00000000000000--- baz 2009-10-14 19:49:59 +0000 +++ quxx 2009-10-14 19:51:00 +0000 @@ -1 +1 @@ -hello +goodbye Binary files bar 2009-10-14 19:49:59 +0000 and qux 2009-10-14 19:50:35 +0000 differ patchkit-0.2.1/test_patches_data/binary.patch000064400000000000000000000002011046102023000174100ustar 00000000000000Binary files bar and qux differ --- baz 2009-10-14 19:49:59 +0000 +++ quxx 2009-10-14 19:51:00 +0000 @@ -1 +1 @@ -hello +goodbye patchkit-0.2.1/test_patches_data/diff000064400000000000000000001327571046102023000157640ustar 00000000000000--- orig/commands.py +++ mod/commands.py @@ -19,25 +19,31 @@ import arch import arch.util import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + import abacmds import cmdutil import shutil import os import options -import paths import time import cmd import readline import re import string -import arch_core -from errors import * -import errors import terminal -import ancillary -import misc import email import smtplib +import textwrap __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" @@ -257,7 +263,7 @@ tree=arch.tree_root() if len(args) == 0: - a_spec = cmdutil.comp_revision(tree) + a_spec = ancillary.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) @@ -284,7 +290,7 @@ changeset=options.changeset tmpdir = None else: - tmpdir=cmdutil.tmpdir() + tmpdir=util.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) @@ -304,14 +310,14 @@ if status > 1: return if (options.perform_diff): - chan = cmdutil.ChangesetMunger(changeset) + chan = arch_compound.ChangesetMunger(changeset) chan.read_indices() - if isinstance(b_spec, arch.Revision): - b_dir = b_spec.library_find() - else: - b_dir = b_spec - a_dir = a_spec.library_find() if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: @@ -517,7 +523,7 @@ except arch.errors.TreeRootError, e: print e return - from_revision=cmdutil.tree_latest(tree) + from_revision = arch_compound.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return @@ -592,6 +598,9 @@ if len(args) == 0: args = None + if options.version is None: + return options, tree.tree_version, args + revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args @@ -601,11 +610,16 @@ """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base else: - base = cmdutil.submit_revision(tree) - + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) @@ -625,18 +639,26 @@ try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: - if cmdutil.prompt("Import from commit"): - return do_import(version) - else: - raise NoVersionLogs(version) - if last_revision!=version.iter_revisions(True).next(): + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: - if not cmdutil.has_changed(version): + if not cmdutil.has_changed(ancestor): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: @@ -645,15 +667,15 @@ raise MissingID(e) else: raise - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) if log is None: try: if cmdutil.prompt("Create log"): - edit_log(tree) + edit_log(tree, version) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: @@ -837,23 +859,24 @@ if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: - revision = cmdutil.comp_revision(tree) + revision = ancillary.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: - munger = cmdutil.MungeOpts() - munger.hunk_prompt = options.hunk_prompt + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: - munger = cmdutil.MungeOpts(True) + munger = cmdutil.arch_compound.MungeOpts(True) munger.all_types(True) if len(args) > 0: - t_cwd = cmdutil.tree_cwd(tree) + t_cwd = arch_compound.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" @@ -878,7 +901,7 @@ if options.pattern_files: munger.add_keep_pattern(options.pattern_files) - for line in cmdutil.revert(tree, revision, munger, + for line in arch_compound.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) @@ -1042,18 +1065,13 @@ help_tree_spec() return -def require_version_exists(version, spec): - if not version.exists(): - raise cmdutil.CantDetermineVersion(spec, - "The version %s does not exist." \ - % version) - class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" + self.cl_revisions = [] def do_command(self, cmdargs): """ @@ -1066,224 +1084,68 @@ self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None + if options.type == "default": + options.type = "archive" try: - iter = self.get_iterator(options.type, args, options.reverse, - options.modified) + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) - + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) - for revision in iter: - log = None - if isinstance(revision, arch.Patchlog): - log = revision - revision=revision.revision - print options.display(revision) - if log is None and (options.summary or options.creator or - options.date or options.merges): - log = revision.patchlog - if options.creator: - print " %s" % log.creator - if options.date: - print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) - if options.summary: - print " %s" % log.summary - if options.merges: - showed_title = False - for revision in log.merged_patches: - if not showed_title: - print " Merged:" - showed_title = True - print " %s" % revision - - def get_iterator(self, type, args, reverse, modified): - if len(args) > 0: - spec = args[0] - else: - spec = None - if modified is not None: - iter = cmdutil.modified_iter(modified, self.tree) - if reverse: - return iter - else: - return cmdutil.iter_reverse(iter) - elif type == "archive": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return version.iter_revisions(reverse) - elif type == "cacherevs": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_cacherevs(version, reverse) - elif type == "library": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - return version.iter_library_revisions(reverse) - elif type == "logs": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ - self.tree), reverse) - elif type == "missing" or type == "skip-present": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - skip = (type == "skip-present") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_missing(self.tree, version, reverse, - skip_present=skip) - - elif type == "present": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_present(self.tree, version, reverse) - - elif type == "new-merges" or type == "direct-merges": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - iter = cmdutil.iter_new_merges(self.tree, version, reverse) - if type == "new-merges": - return iter - elif type == "direct-merges": - return cmdutil.direct_merges(iter) - - elif type == "missing-from": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - revision = cmdutil.determine_revision_tree(self.tree, spec) - libtree = cmdutil.find_or_make_local_revision(revision) - return cmdutil.iter_missing(libtree, self.tree.tree_version, - reverse) - - elif type == "partner-missing": - return cmdutil.iter_partner_missing(self.tree, reverse) - - elif type == "ancestry": - revision = cmdutil.determine_revision_tree(self.tree, spec) - iter = cmdutil._iter_ancestry(self.tree, revision) - if reverse: - return iter - else: - return cmdutil.iter_reverse(iter) - - elif type == "dependencies" or type == "non-dependencies": - nondeps = (type == "non-dependencies") - revision = cmdutil.determine_revision_tree(self.tree, spec) - anc_iter = cmdutil._iter_ancestry(self.tree, revision) - iter_depends = cmdutil.iter_depends(anc_iter, nondeps) - if reverse: - return iter_depends - else: - return cmdutil.iter_reverse(iter_depends) - elif type == "micro": - return cmdutil.iter_micro(self.tree) - - + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ - parser=cmdutil.CmdOptionParser("fai revisions [revision]") + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") - select.add_option("", "--archive", action="store_const", - const="archive", dest="type", default="archive", - help="List all revisions in the archive") - select.add_option("", "--cacherevs", action="store_const", - const="cacherevs", dest="type", - help="List all revisions stored in the archive as " - "complete copies") - select.add_option("", "--logs", action="store_const", - const="logs", dest="type", - help="List revisions that have a patchlog in the " - "tree") - select.add_option("", "--missing", action="store_const", - const="missing", dest="type", - help="List revisions from the specified version that" - " have no patchlog in the tree") - select.add_option("", "--skip-present", action="store_const", - const="skip-present", dest="type", - help="List revisions from the specified version that" - " have no patchlogs at all in the tree") - select.add_option("", "--present", action="store_const", - const="present", dest="type", - help="List revisions from the specified version that" - " have no patchlog in the tree, but can't be merged") - select.add_option("", "--missing-from", action="store_const", - const="missing-from", dest="type", - help="List revisions from the specified revision " - "that have no patchlog for the tree version") - select.add_option("", "--partner-missing", action="store_const", - const="partner-missing", dest="type", - help="List revisions in partner versions that are" - " missing") - select.add_option("", "--new-merges", action="store_const", - const="new-merges", dest="type", - help="List revisions that have had patchlogs added" - " to the tree since the last commit") - select.add_option("", "--direct-merges", action="store_const", - const="direct-merges", dest="type", - help="List revisions that have been directly added" - " to tree since the last commit ") - select.add_option("", "--library", action="store_const", - const="library", dest="type", - help="List revisions in the revision library") - select.add_option("", "--ancestry", action="store_const", - const="ancestry", dest="type", - help="List revisions that are ancestors of the " - "current tree version") - - select.add_option("", "--dependencies", action="store_const", - const="dependencies", dest="type", - help="List revisions that the given revision " - "depends on") - - select.add_option("", "--non-dependencies", action="store_const", - const="non-dependencies", dest="type", - help="List revisions that the given revision " - "does not depend on") - - select.add_option("--micro", action="store_const", - const="micro", dest="type", - help="List partner revisions aimed for this " - "micro-branch") - - select.add_option("", "--modified", dest="modified", - help="List tree ancestor revisions that modified a " - "given file", metavar="FILE[:LINE]") + cmdutil.add_revision_iter_options(select) parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", @@ -1312,6 +1174,9 @@ format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") @@ -1448,6 +1313,7 @@ if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) + self.cwd = os.getcwd() def write_history(self): readline.write_history_file(self.history_file) @@ -1470,16 +1336,21 @@ def set_prompt(self): if self.tree is not None: try: - version = " "+self.tree.tree_version.nonarch + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt except: - version = "" + prompt = "" else: - version = "" - self.prompt = "Fai%s> " % version + prompt = "" + self.prompt = "Fai%s> " % prompt def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1489,8 +1360,15 @@ def do_cd(self, line): if line == "": line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) try: - os.chdir(os.path.expanduser(line)) + os.chdir(newcwd) + self.cwd = newcwd except Exception, e: print e try: @@ -1523,7 +1401,7 @@ except cmdutil.CantDetermineRevision, e: print e except Exception, e: - print "Unhandled error:\n%s" % cmdutil.exception_str(e) + print "Unhandled error:\n%s" % errors.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] @@ -1574,7 +1452,7 @@ arg = line.split()[-1] else: arg = "" - iter = iter_munged_completions(iter, arg, text) + iter = cmdutil.iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) @@ -1604,10 +1482,11 @@ else: arg = "" if arg.startswith("-"): - return list(iter_munged_completions(iter, arg, text)) + return list(cmdutil.iter_munged_completions(iter, arg, + text)) else: - return list(iter_munged_completions( - iter_file_completions(arg), arg, text)) + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) elif cmd == "cd": @@ -1615,13 +1494,13 @@ arg = args.split()[-1] else: arg = "" - iter = iter_dir_completions(arg) - iter = iter_munged_completions(iter, arg, text) + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] - return list(iter_munged_completions(iter_file_completions(arg), - arg, text)) + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: @@ -1636,44 +1515,8 @@ yield entry -def iter_file_completions(arg, only_dirs = False): - """Generate an iterator that iterates through filename completions. - - :param arg: The filename fragment to match - :type arg: str - :param only_dirs: If true, match only directories - :type only_dirs: bool - """ - cwd = os.getcwd() - if cwd != "/": - extras = [".", ".."] - else: - extras = [] - (dir, file) = os.path.split(arg) - if dir != "": - listingdir = os.path.expanduser(dir) - else: - listingdir = cwd - for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): - if dir != "": - userfile = dir+'/'+file - else: - userfile = file - if userfile.startswith(arg): - if os.path.isdir(listingdir+'/'+file): - userfile+='/' - yield userfile - elif not only_dirs: - yield userfile - -def iter_munged_completions(iter, arg, text): - for completion in iter: - completion = str(completion) - if completion.startswith(arg): - yield completion[len(arg)-len(text):] - def iter_source_file_completions(tree, arg): - treepath = cmdutil.tree_cwd(tree) + treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: @@ -1701,7 +1544,7 @@ :return: An iterator of all matching untagged files :rtype: iterator of str """ - treepath = cmdutil.tree_cwd(tree) + treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: @@ -1743,8 +1586,8 @@ :param arg: The prefix to match :type arg: str """ - treepath = cmdutil.tree_cwd(tree) - tmpdir = cmdutil.tmpdir() + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) @@ -1756,14 +1599,6 @@ shutil.rmtree(tmpdir) return completions -def iter_dir_completions(arg): - """Generate an iterator that iterates through directory name completions. - - :param arg: The directory name fragment to match - :type arg: str - """ - return iter_file_completions(arg, True) - class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" @@ -1795,7 +1630,11 @@ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) - tree = arch.tree_root() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp @@ -1809,13 +1648,22 @@ if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): - if method == "explicit": - options.id_type == explicit + if method == "explicit" or method == "implicit": + options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): @@ -1824,7 +1672,8 @@ return if options.id_type == "auto": - if method != "tagline" and method != "explicit": + if method != "tagline" and method != "explicit" \ + and method !="implicit": print "add-id not supported for \"%s\" tagging method" % method return else: @@ -1852,10 +1701,12 @@ previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) - elif id_type == "tagline": + elif id_type == "tagline" or id_type == "implicit": for file in files: try: - cmdutil.add_tagline_or_explicit_id(file) + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: @@ -1888,6 +1739,9 @@ parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") @@ -1926,27 +1780,7 @@ def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError - completions = list(ancillary.iter_partners(self.tree, - self.tree.tree_version)) - if len(completions) == 0: - completions = list(self.tree.iter_log_versions()) - - aliases = [] - try: - for completion in completions: - alias = ancillary.compact_alias(str(completion), self.tree) - if alias: - aliases.extend(alias) - - for completion in completions: - if completion.archive == self.tree.tree_version.archive: - aliases.append(completion.nonarch) - - except Exception, e: - print e - - completions.extend(aliases) - return completions + return cmdutil.merge_completions(self.tree, arg, index) def do_command(self, cmdargs): """ @@ -1961,7 +1795,7 @@ if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) - if cmdutil.has_changed(self.tree.tree_version): + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): raise UncommittedChanges(self.tree) if len(args) > 0: @@ -2027,14 +1861,14 @@ :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ - other_tree = cmdutil.find_or_make_local_revision(other_revision) + other_tree = arch_compound.find_or_make_local_revision(other_revision) try: if action == "native-merge": - ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, - other_revision) + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) elif action == "update": - ancestor = cmdutil.tree_latest(self.tree, - other_revision.version) + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) @@ -2104,7 +1938,10 @@ if self.tree is None: raise arch.errors.TreeRootError - edit_log(self.tree) + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) def get_parser(self): """ @@ -2132,7 +1969,7 @@ """ return -def edit_log(tree): +def edit_log(tree, version): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge @@ -2141,28 +1978,29 @@ """ #ensure we have an editor before preparing the log cmdutil.find_editor() - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) - log = tree.log_message(create=True) + log = tree.log_message(create=True, version=version) log_is_new = True tmplog = log.name - template = tree+"/{arch}/=log-template" - if not os.path.exists(template): - template = os.path.expanduser("~/.arch-params/=log-template") - if not os.path.exists(template): - template = None + template = pylon.log_template_path(tree) if template: shutil.copyfile(template, tmplog) - - new_merges = list(cmdutil.iter_new_merges(tree, - tree.tree_version)) - log["Summary"] = merge_summary(new_merges, tree.tree_version) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: @@ -2172,29 +2010,6 @@ os.remove(log.name) raise -def merge_summary(new_merges, tree_version): - if len(new_merges) == 0: - return "" - if len(new_merges) == 1: - summary = new_merges[0].summary - else: - summary = "Merge" - - credits = [] - for merge in new_merges: - if arch.my_id() != merge.creator: - name = re.sub("<.*>", "", merge.creator).rstrip(" "); - if not name in credits: - credits.append(name) - else: - version = merge.revision.version - if version.archive == tree_version.archive: - if not version.nonarch in credits: - credits.append(version.nonarch) - elif not str(version) in credits: - credits.append(str(version)) - - return ("%s (%s)") % (summary, ", ".join(credits)) class MirrorArchive(BaseCommand): """ @@ -2268,31 +2083,73 @@ Use "alias" to list available (user and automatic) aliases.""" +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + def help_aliases(tree): - print """Auto-generated aliases - acur : The latest revision in the archive of the tree-version. You can specfy - a different version like so: acur:foo--bar--0 (aliases can be used) - tcur : (tree current) The latest revision in the tree of the tree-version. - You can specify a different version like so: tcur:foo--bar--0 (aliases - can be used). -tprev : (tree previous) The previous revision in the tree of the tree-version. - To specify an older revision, use a number, e.g. "tprev:4" - tanc : (tree ancestor) The ancestor revision of the tree - To specify an older revision, use a number, e.g. "tanc:4" -tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") - tmod : (tree modified) The latest revision to modify a given file - (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") - ttag : (tree tag) The revision that was tagged into the current tree revision, - according to the tree. -tagcur: (tag current) The latest revision of the version that the current tree - was tagged from. -mergeanc : The common ancestor of the current tree and the specified revision. - Defaults to the first partner-version's latest revision or to tagcur. - """ + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) print "User aliases" - for parts in ancillary.iter_all_alias(tree): - print parts[0].rjust(10)+" : "+parts[1] - + display_def(ancillary.iter_all_alias(tree)) class Inventory(BaseCommand): """List the status of files in the tree""" @@ -2428,6 +2285,11 @@ except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments @@ -2438,15 +2300,20 @@ if len(args) == 0: help_aliases(self.tree) return - elif len(args) == 1: - self.print_alias(args[0]) - elif (len(args)) == 2: - self.add(args[0], args[1], options) else: - raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp def print_alias(self, alias): answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] @@ -2464,6 +2331,8 @@ :type expansion: str :param options: The commandline options """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, @@ -2490,14 +2359,17 @@ deleted = False if len(args) != 1: raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) newlist = "" for pair in self.get_iterator(options): - if pair[0] != args[0]: + if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: - raise errors.NoSuchAlias(args[0]) + raise errors.NoSuchAlias(alias) self.write_aliases(newlist, options) def get_alias_file(self, options): @@ -2526,7 +2398,7 @@ :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) - file = cmdutil.NewFileVersion(filename) + file = util.NewFileVersion(filename) file.write(newlist) file.commit() @@ -2588,10 +2460,13 @@ :param cmdargs: The commandline arguments :type cmdargs: list of str """ - cmdutil.find_editor() parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: self.tree=arch.tree_root() except: self.tree=None @@ -2655,7 +2530,7 @@ target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: - target_revision = cmdutil.tree_latest(self.tree) + target_revision = arch_compound.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] @@ -2711,7 +2586,7 @@ :param message: The message to send :type message: `email.Message`""" - server = smtplib.SMTP() + server = smtplib.SMTP("localhost") server.sendmail(message['From'], message['To'], message.as_string()) server.quit() @@ -2763,6 +2638,22 @@ 'alias' : Alias, 'request-merge': RequestMerge, } + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", @@ -2784,6 +2675,7 @@ 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", -'file-revert' : "Use revert FILE" +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.2.1/test_patches_data/diff-2000064400000000000000000000004161046102023000161050ustar 00000000000000--- patches.py +++ patches.py @@ -391,6 +391,8 @@ else: assert isinstance(hunk_line, RemoveLine) line_no += 1 + for line in orig_lines: + yield line import unittest import os.path patchkit-0.2.1/test_patches_data/diff-3000064400000000000000000000003141046102023000161030ustar 00000000000000--- orig-3 2005-09-23 16:23:20.000000000 -0500 +++ mod-3 2005-09-23 16:23:38.000000000 -0500 @@ -1,3 +1,4 @@ +First line change # Copyright (C) 2004, 2005 Aaron Bentley # # patchkit-0.2.1/test_patches_data/diff-4000064400000000000000000000003371046102023000161110ustar 00000000000000--- orig-4 2005-09-23 16:24:21.000000000 -0500 +++ mod-4 2005-09-23 16:24:35.000000000 -0500 @@ -555,4 +555,4 @@ if __name__ == "__main__": test() -# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 +last line change patchkit-0.2.1/test_patches_data/diff-5000064400000000000000000000116401046102023000161110ustar 00000000000000--- orig-5 2005-09-23 16:25:00.000000000 -0500 +++ mod-5 2005-09-23 16:25:21.000000000 -0500 @@ -60,161 +60,6 @@ raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) -def parse_range(textrange): - """Parse a patch range, handling the "1" special-case - - :param textrange: The text to parse - :type textrange: str - :return: the position and range, as a tuple - :rtype: (int, int) - """ - tmp = textrange.split(',') - if len(tmp) == 1: - pos = tmp[0] - range = "1" - else: - (pos, range) = tmp - pos = int(pos) - range = int(range) - return (pos, range) - - -def hunk_from_header(line): - if not line.startswith("@@") or not line.endswith("@@\n") \ - or not len(line) > 4: - raise MalformedHunkHeader("Does not start and end with @@.", line) - try: - (orig, mod) = line[3:-4].split(" ") - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if not orig.startswith('-') or not mod.startswith('+'): - raise MalformedHunkHeader("Positions don't start with + or -.", line) - try: - (orig_pos, orig_range) = parse_range(orig[1:]) - (mod_pos, mod_range) = parse_range(mod[1:]) - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if mod_range < 0 or orig_range < 0: - raise MalformedHunkHeader("Hunk range is negative", line) - return Hunk(orig_pos, orig_range, mod_pos, mod_range) - - -class HunkLine: - def __init__(self, contents): - self.contents = contents - - def get_str(self, leadchar): - if self.contents == "\n" and leadchar == " " and False: - return "\n" - if not self.contents.endswith('\n'): - terminator = '\n' + NO_NL - else: - terminator = '' - return leadchar + self.contents + terminator - - -class ContextLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str(" ") - - -class InsertLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("+") - - -class RemoveLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("-") - -NO_NL = '\\ No newline at end of file\n' -__pychecker__="no-returnvalues" - -def parse_line(line): - if line.startswith("\n"): - return ContextLine(line) - elif line.startswith(" "): - return ContextLine(line[1:]) - elif line.startswith("+"): - return InsertLine(line[1:]) - elif line.startswith("-"): - return RemoveLine(line[1:]) - elif line == NO_NL: - return NO_NL - else: - raise MalformedLine("Unknown line type", line) -__pychecker__="" - - -class Hunk: - def __init__(self, orig_pos, orig_range, mod_pos, mod_range): - self.orig_pos = orig_pos - self.orig_range = orig_range - self.mod_pos = mod_pos - self.mod_range = mod_range - self.lines = [] - - def get_header(self): - return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, - self.orig_range), - self.range_str(self.mod_pos, - self.mod_range)) - - def range_str(self, pos, range): - """Return a file range, special-casing for 1-line files. - - :param pos: The position in the file - :type pos: int - :range: The range in the file - :type range: int - :return: a string in the format 1,4 except when range == pos == 1 - """ - if range == 1: - return "%i" % pos - else: - return "%i,%i" % (pos, range) - - def __str__(self): - lines = [self.get_header()] - for line in self.lines: - lines.append(str(line)) - return "".join(lines) - - def shift_to_mod(self, pos): - if pos < self.orig_pos-1: - return 0 - elif pos > self.orig_pos+self.orig_range: - return self.mod_range - self.orig_range - else: - return self.shift_to_mod_lines(pos) - - def shift_to_mod_lines(self, pos): - assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) - position = self.orig_pos-1 - shift = 0 - for line in self.lines: - if isinstance(line, InsertLine): - shift += 1 - elif isinstance(line, RemoveLine): - if position == pos: - return None - shift -= 1 - position += 1 - elif isinstance(line, ContextLine): - position += 1 - if position > pos: - break - return shift - def iter_hunks(iter_lines): hunk = None for line in iter_lines: patchkit-0.2.1/test_patches_data/diff-6000064400000000000000000000453641046102023000161240ustar 00000000000000--- orig-6 2005-09-23 16:27:16.000000000 -0500 +++ mod-6 2005-09-23 16:27:32.000000000 -0500 @@ -1,558 +1 @@ -# Copyright (C) 2004, 2005 Aaron Bentley -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -class PatchSyntax(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - - -class MalformedPatchHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedHunkHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedLine(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed line. %s\n%s" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -def get_patch_names(iter_lines): - try: - line = iter_lines.next() - if not line.startswith("--- "): - raise MalformedPatchHeader("No orig name", line) - else: - orig_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No orig line", "") - try: - line = iter_lines.next() - if not line.startswith("+++ "): - raise PatchSyntax("No mod name") - else: - mod_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No mod line", "") - return (orig_name, mod_name) - -def parse_range(textrange): - """Parse a patch range, handling the "1" special-case - - :param textrange: The text to parse - :type textrange: str - :return: the position and range, as a tuple - :rtype: (int, int) - """ - tmp = textrange.split(',') - if len(tmp) == 1: - pos = tmp[0] - range = "1" - else: - (pos, range) = tmp - pos = int(pos) - range = int(range) - return (pos, range) - - -def hunk_from_header(line): - if not line.startswith("@@") or not line.endswith("@@\n") \ - or not len(line) > 4: - raise MalformedHunkHeader("Does not start and end with @@.", line) - try: - (orig, mod) = line[3:-4].split(" ") - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if not orig.startswith('-') or not mod.startswith('+'): - raise MalformedHunkHeader("Positions don't start with + or -.", line) - try: - (orig_pos, orig_range) = parse_range(orig[1:]) - (mod_pos, mod_range) = parse_range(mod[1:]) - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if mod_range < 0 or orig_range < 0: - raise MalformedHunkHeader("Hunk range is negative", line) - return Hunk(orig_pos, orig_range, mod_pos, mod_range) - - -class HunkLine: - def __init__(self, contents): - self.contents = contents - - def get_str(self, leadchar): - if self.contents == "\n" and leadchar == " " and False: - return "\n" - if not self.contents.endswith('\n'): - terminator = '\n' + NO_NL - else: - terminator = '' - return leadchar + self.contents + terminator - - -class ContextLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str(" ") - - -class InsertLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("+") - - -class RemoveLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("-") - -NO_NL = '\\ No newline at end of file\n' -__pychecker__="no-returnvalues" - -def parse_line(line): - if line.startswith("\n"): - return ContextLine(line) - elif line.startswith(" "): - return ContextLine(line[1:]) - elif line.startswith("+"): - return InsertLine(line[1:]) - elif line.startswith("-"): - return RemoveLine(line[1:]) - elif line == NO_NL: - return NO_NL - else: - raise MalformedLine("Unknown line type", line) -__pychecker__="" - - -class Hunk: - def __init__(self, orig_pos, orig_range, mod_pos, mod_range): - self.orig_pos = orig_pos - self.orig_range = orig_range - self.mod_pos = mod_pos - self.mod_range = mod_range - self.lines = [] - - def get_header(self): - return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, - self.orig_range), - self.range_str(self.mod_pos, - self.mod_range)) - - def range_str(self, pos, range): - """Return a file range, special-casing for 1-line files. - - :param pos: The position in the file - :type pos: int - :range: The range in the file - :type range: int - :return: a string in the format 1,4 except when range == pos == 1 - """ - if range == 1: - return "%i" % pos - else: - return "%i,%i" % (pos, range) - - def __str__(self): - lines = [self.get_header()] - for line in self.lines: - lines.append(str(line)) - return "".join(lines) - - def shift_to_mod(self, pos): - if pos < self.orig_pos-1: - return 0 - elif pos > self.orig_pos+self.orig_range: - return self.mod_range - self.orig_range - else: - return self.shift_to_mod_lines(pos) - - def shift_to_mod_lines(self, pos): - assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) - position = self.orig_pos-1 - shift = 0 - for line in self.lines: - if isinstance(line, InsertLine): - shift += 1 - elif isinstance(line, RemoveLine): - if position == pos: - return None - shift -= 1 - position += 1 - elif isinstance(line, ContextLine): - position += 1 - if position > pos: - break - return shift - -def iter_hunks(iter_lines): - hunk = None - for line in iter_lines: - if line == "\n": - if hunk is not None: - yield hunk - hunk = None - continue - if hunk is not None: - yield hunk - hunk = hunk_from_header(line) - orig_size = 0 - mod_size = 0 - while orig_size < hunk.orig_range or mod_size < hunk.mod_range: - hunk_line = parse_line(iter_lines.next()) - hunk.lines.append(hunk_line) - if isinstance(hunk_line, (RemoveLine, ContextLine)): - orig_size += 1 - if isinstance(hunk_line, (InsertLine, ContextLine)): - mod_size += 1 - if hunk is not None: - yield hunk - -class Patch: - def __init__(self, oldname, newname): - self.oldname = oldname - self.newname = newname - self.hunks = [] - - def __str__(self): - ret = self.get_header() - ret += "".join([str(h) for h in self.hunks]) - return ret - - def get_header(self): - return "--- %s\n+++ %s\n" % (self.oldname, self.newname) - - def stats_str(self): - """Return a string of patch statistics""" - removes = 0 - inserts = 0 - for hunk in self.hunks: - for line in hunk.lines: - if isinstance(line, InsertLine): - inserts+=1; - elif isinstance(line, RemoveLine): - removes+=1; - return "%i inserts, %i removes in %i hunks" % \ - (inserts, removes, len(self.hunks)) - - def pos_in_mod(self, position): - newpos = position - for hunk in self.hunks: - shift = hunk.shift_to_mod(position) - if shift is None: - return None - newpos += shift - return newpos - - def iter_inserted(self): - """Iteraties through inserted lines - - :return: Pair of line number, line - :rtype: iterator of (int, InsertLine) - """ - for hunk in self.hunks: - pos = hunk.mod_pos - 1; - for line in hunk.lines: - if isinstance(line, InsertLine): - yield (pos, line) - pos += 1 - if isinstance(line, ContextLine): - pos += 1 - -def parse_patch(iter_lines): - (orig_name, mod_name) = get_patch_names(iter_lines) - patch = Patch(orig_name, mod_name) - for hunk in iter_hunks(iter_lines): - patch.hunks.append(hunk) - return patch - - -def iter_file_patch(iter_lines): - saved_lines = [] - for line in iter_lines: - if line.startswith('=== '): - continue - elif line.startswith('--- '): - if len(saved_lines) > 0: - yield saved_lines - saved_lines = [] - saved_lines.append(line) - if len(saved_lines) > 0: - yield saved_lines - - -def iter_lines_handle_nl(iter_lines): - """ - Iterates through lines, ensuring that lines that originally had no - terminating \n are produced without one. This transformation may be - applied at any point up until hunk line parsing, and is safe to apply - repeatedly. - """ - last_line = None - for line in iter_lines: - if line == NO_NL: - assert last_line.endswith('\n') - last_line = last_line[:-1] - line = None - if last_line is not None: - yield last_line - last_line = line - if last_line is not None: - yield last_line - - -def parse_patches(iter_lines): - iter_lines = iter_lines_handle_nl(iter_lines) - return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] - - -def difference_index(atext, btext): - """Find the indext of the first character that differs betweeen two texts - - :param atext: The first text - :type atext: str - :param btext: The second text - :type str: str - :return: The index, or None if there are no differences within the range - :rtype: int or NoneType - """ - length = len(atext) - if len(btext) < length: - length = len(btext) - for i in range(length): - if atext[i] != btext[i]: - return i; - return None - -class PatchConflict(Exception): - def __init__(self, line_no, orig_line, patch_line): - orig = orig_line.rstrip('\n') - patch = str(patch_line).rstrip('\n') - msg = 'Text contents mismatch at line %d. Original has "%s",'\ - ' but patch says it should be "%s"' % (line_no, orig, patch) - Exception.__init__(self, msg) - - -def iter_patched(orig_lines, patch_lines): - """Iterate through a series of lines with a patch applied. - This handles a single file, and does exact, not fuzzy patching. - """ - if orig_lines is not None: - orig_lines = orig_lines.__iter__() - seen_patch = [] - patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) - get_patch_names(patch_lines) - line_no = 1 - for hunk in iter_hunks(patch_lines): - while line_no < hunk.orig_pos: - orig_line = orig_lines.next() - yield orig_line - line_no += 1 - for hunk_line in hunk.lines: - seen_patch.append(str(hunk_line)) - if isinstance(hunk_line, InsertLine): - yield hunk_line.contents - elif isinstance(hunk_line, (ContextLine, RemoveLine)): - orig_line = orig_lines.next() - if orig_line != hunk_line.contents: - raise PatchConflict(line_no, orig_line, "".join(seen_patch)) - if isinstance(hunk_line, ContextLine): - yield orig_line - else: - assert isinstance(hunk_line, RemoveLine) - line_no += 1 - -import unittest -import os.path -class PatchesTester(unittest.TestCase): - def datafile(self, filename): - data_path = os.path.join(os.path.dirname(__file__), "testdata", - filename) - return file(data_path, "rb") - - def testValidPatchHeader(self): - """Parse a valid patch header""" - lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') - (orig, mod) = get_patch_names(lines.__iter__()) - assert(orig == "orig/commands.py") - assert(mod == "mod/dommands.py") - - def testInvalidPatchHeader(self): - """Parse an invalid patch header""" - lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') - self.assertRaises(MalformedPatchHeader, get_patch_names, - lines.__iter__()) - - def testValidHunkHeader(self): - """Parse a valid hunk header""" - header = "@@ -34,11 +50,6 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 34) - assert (hunk.orig_range == 11) - assert (hunk.mod_pos == 50) - assert (hunk.mod_range == 6) - assert (str(hunk) == header) - - def testValidHunkHeader2(self): - """Parse a tricky, valid hunk header""" - header = "@@ -1 +0,0 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 1) - assert (hunk.orig_range == 1) - assert (hunk.mod_pos == 0) - assert (hunk.mod_range == 0) - assert (str(hunk) == header) - - def makeMalformed(self, header): - self.assertRaises(MalformedHunkHeader, hunk_from_header, header) - - def testInvalidHeader(self): - """Parse an invalid hunk header""" - self.makeMalformed(" -34,11 +50,6 \n") - self.makeMalformed("@@ +50,6 -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6 @@") - self.makeMalformed("@@ -34.5,11 +50,6 @@\n") - self.makeMalformed("@@-34,11 +50,6@@\n") - self.makeMalformed("@@ 34,11 50,6 @@\n") - self.makeMalformed("@@ -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6.5 @@\n") - self.makeMalformed("@@ -34,11 +50,-6 @@\n") - - def lineThing(self,text, type): - line = parse_line(text) - assert(isinstance(line, type)) - assert(str(line)==text) - - def makeMalformedLine(self, text): - self.assertRaises(MalformedLine, parse_line, text) - - def testValidLine(self): - """Parse a valid hunk line""" - self.lineThing(" hello\n", ContextLine) - self.lineThing("+hello\n", InsertLine) - self.lineThing("-hello\n", RemoveLine) - - def testMalformedLine(self): - """Parse invalid valid hunk lines""" - self.makeMalformedLine("hello\n") - - def compare_parsed(self, patchtext): - lines = patchtext.splitlines(True) - patch = parse_patch(lines.__iter__()) - pstr = str(patch) - i = difference_index(patchtext, pstr) - if i is not None: - print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) - self.assertEqual (patchtext, str(patch)) - - def testAll(self): - """Test parsing a whole patch""" - patchtext = """--- orig/commands.py -+++ mod/commands.py -@@ -1337,7 +1337,8 @@ - - def set_title(self, command=None): - try: -- version = self.tree.tree_version.nonarch -+ version = pylon.alias_or_version(self.tree.tree_version, self.tree, -+ full=False) - except: - version = "[no version]" - if command is None: -@@ -1983,7 +1984,11 @@ - version) - if len(new_merges) > 0: - if cmdutil.prompt("Log for merge"): -- mergestuff = cmdutil.log_for_merge(tree, comp_version) -+ if cmdutil.prompt("changelog for merge"): -+ mergestuff = "Patches applied:\\n" -+ mergestuff += pylon.changelog_for_merge(new_merges) -+ else: -+ mergestuff = cmdutil.log_for_merge(tree, comp_version) - log.description += mergestuff - log.save() - try: -""" - self.compare_parsed(patchtext) - - def testInit(self): - """Handle patches missing half the position, range tuple""" - patchtext = \ -"""--- orig/__init__.py -+++ mod/__init__.py -@@ -1 +1,2 @@ - __docformat__ = "restructuredtext en" -+__doc__ = An alternate Arch commandline interface -""" - self.compare_parsed(patchtext) - - - - def testLineLookup(self): - import sys - """Make sure we can accurately look up mod line from orig""" - patch = parse_patch(self.datafile("diff")) - orig = list(self.datafile("orig")) - mod = list(self.datafile("mod")) - removals = [] - for i in range(len(orig)): - mod_pos = patch.pos_in_mod(i) - if mod_pos is None: - removals.append(orig[i]) - continue - assert(mod[mod_pos]==orig[i]) - rem_iter = removals.__iter__() - for hunk in patch.hunks: - for line in hunk.lines: - if isinstance(line, RemoveLine): - next = rem_iter.next() - if line.contents != next: - sys.stdout.write(" orig:%spatch:%s" % (next, - line.contents)) - assert(line.contents == next) - self.assertRaises(StopIteration, rem_iter.next) - - def testFirstLineRenumber(self): - """Make sure we handle lines at the beginning of the hunk""" - patch = parse_patch(self.datafile("insert_top.patch")) - assert (patch.pos_in_mod(0)==1) - -def test(): - patchesTestSuite = unittest.makeSuite(PatchesTester,'test') - runner = unittest.TextTestRunner(verbosity=0) - return runner.run(patchesTestSuite) - - -if __name__ == "__main__": - test() -# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 +Total contents change patchkit-0.2.1/test_patches_data/diff-7000064400000000000000000000003141046102023000161070ustar 00000000000000--- orig-7 2008-12-02 01:58:43.000000000 +0100 +++ mod-7 2008-12-02 01:58:43.000000000 +0100 @@ -1 +1 @@ -No terminating newline \ No newline at end of file +No newline either \ No newline at end of file patchkit-0.2.1/test_patches_data/insert_top.patch000064400000000000000000000002021046102023000203130ustar 00000000000000--- orig/pylon/patches.py +++ mod/pylon/patches.py @@ -1,3 +1,4 @@ +#test import util import sys class PatchSyntax(Exception): patchkit-0.2.1/test_patches_data/mod000064400000000000000000002677631046102023000156410ustar 00000000000000# Copyright (C) 2004 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import sys import arch import arch.util import arch.arch import pylon.errors from pylon.errors import * from pylon import errors from pylon import util from pylon import arch_core from pylon import arch_compound from pylon import ancillary from pylon import misc from pylon import paths import abacmds import cmdutil import shutil import os import options import time import cmd import readline import re import string import terminal import email import smtplib import textwrap __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" commands = {} def find_command(cmd): """ Return an instance of a command type. Return None if the type isn't registered. :param cmd: the name of the command to look for :type cmd: the type of the command """ if commands.has_key(cmd): return commands[cmd]() else: return None class BaseCommand: def __call__(self, cmdline): try: self.do_command(cmdline.split()) except cmdutil.GetHelp, e: self.help() except Exception, e: print e def get_completer(index): return None def complete(self, args, text): """ Returns a list of possible completions for the given text. :param args: The complete list of arguments :type args: List of str :param text: text to complete (may be shorter than args[-1]) :type text: str :rtype: list of str """ matches = [] candidates = None if len(args) > 0: realtext = args[-1] else: realtext = "" try: parser=self.get_parser() if realtext.startswith('-'): candidates = parser.iter_options() else: (options, parsed_args) = parser.parse_args(args) if len (parsed_args) > 0: candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) else: candidates = self.get_completer("", 0) except: pass if candidates is None: return for candidate in candidates: candidate = str(candidate) if candidate.startswith(realtext): matches.append(candidate[len(realtext)- len(text):]) return matches class Help(BaseCommand): """ Lists commands, prints help messages. """ def __init__(self): self.description="Prints help mesages" self.parser = None def do_command(self, cmdargs): """ Prints a help message. """ options, args = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp if options.native or options.suggestions or options.external: native = options.native suggestions = options.suggestions external = options.external else: native = True suggestions = False external = True if len(args) == 0: self.list_commands(native, suggestions, external) return elif len(args) == 1: command_help(args[0]) return def help(self): self.get_parser().print_help() print """ If no command is specified, commands are listed. If a command is specified, help for that command is listed. """ def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ if self.parser is not None: return self.parser parser=cmdutil.CmdOptionParser("fai help [command]") parser.add_option("-n", "--native", action="store_true", dest="native", help="Show native commands") parser.add_option("-e", "--external", action="store_true", dest="external", help="Show external commands") parser.add_option("-s", "--suggest", action="store_true", dest="suggestions", help="Show suggestions") self.parser = parser return parser def list_commands(self, native=True, suggest=False, external=True): """ Lists supported commands. :param native: list native, python-based commands :type native: bool :param external: list external aba-style commands :type external: bool """ if native: print "Native Fai commands" keys=commands.keys() keys.sort() for k in keys: space="" for i in range(28-len(k)): space+=" " print space+k+" : "+commands[k]().description print if suggest: print "Unavailable commands and suggested alternatives" key_list = suggestions.keys() key_list.sort() for key in key_list: print "%28s : %s" % (key, suggestions[key]) print if external: fake_aba = abacmds.AbaCmds() if (fake_aba.abadir == ""): return print "External commands" fake_aba.list_commands() print if not suggest: print "Use help --suggest to list alternatives to tla and aba"\ " commands." if options.tla_fallthrough and (native or external): print "Fai also supports tla commands." def command_help(cmd): """ Prints help for a command. :param cmd: The name of the command to print help for :type cmd: str """ fake_aba = abacmds.AbaCmds() cmdobj = find_command(cmd) if cmdobj != None: cmdobj.help() elif suggestions.has_key(cmd): print "Not available\n" + suggestions[cmd] else: abacmd = fake_aba.is_command(cmd) if abacmd: abacmd.help() else: print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" class Changes(BaseCommand): """ the "changes" command: lists differences between trees/revisions: """ def __init__(self): self.description="Lists what files have changed in the project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp tree=arch.tree_root() if len(args) == 0: a_spec = ancillary.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) if len(args) == 2: b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) else: b_spec=tree return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that perfoms the "changes" command. """ try: options, a_spec, b_spec = self.parse_commandline(cmdargs); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return if options.changeset: changeset=options.changeset tmpdir = None else: tmpdir=util.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) try: for line in delta: if cmdutil.chattermatch(line, "changeset:"): pass else: cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise status=delta.status if status > 1: return if (options.perform_diff): chan = arch_compound.ChangesetMunger(changeset) chan.read_indices() if options.diffopts is not None: if isinstance(b_spec, arch.Revision): b_dir = b_spec.library_find() else: b_dir = b_spec a_dir = a_spec.library_find() diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: cmdutil.show_diffs(delta.changeset) finally: if tmpdir and (os.access(tmpdir, os.X_OK)): shutil.rmtree(tmpdir) def get_parser(self): """ Returns the options parser to use for the "changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" " [revision]") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") parser.add_option("--diffopts", dest="diffopts", help="Use the specified diff options", metavar="OPTIONS") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Performs source-tree comparisons If no revision is specified, the current project tree is compared to the last-committed revision. If one revision is specified, the current project tree is compared to that revision. If two revisions are specified, they are compared to each other. """ help_tree_spec() return class ApplyChanges(BaseCommand): """ Apply differences between two revisions to a tree """ def __init__(self): self.description="Applies changes to a project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) != 2: raise cmdutil.GetHelp a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that performs "apply-changes". """ try: tree = arch.tree_root() options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return delta=cmdutil.apply_delta(a_spec, b_spec, tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line, options.suppress_chatter) def get_parser(self): """ Returns the options parser to use for the "apply-changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" " revision") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Applies changes to a project tree Compares two revisions and applies the difference between them to the current tree. """ help_tree_spec() return class Update(BaseCommand): """ Updates a project tree to a given revision, preserving un-committed hanges. """ def __init__(self): self.description="Apply the latest changes to the current directory" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp spec=None if len(args)>0: spec=args[0] revision=cmdutil.determine_revision_arch(tree, spec) cmdutil.ensure_archive_registered(revision.archive) mirror_source = cmdutil.get_mirror_source(revision.archive) if mirror_source != None: if cmdutil.prompt("Mirror update"): cmd=cmdutil.mirror_archive(mirror_source, revision.archive, arch.NameParser(revision).get_package_version()) for line in arch.chatter_classifier(cmd): cmdutil.colorize(line, options.suppress_chatter) revision=cmdutil.determine_revision_arch(tree, spec) return options, revision def do_command(self, cmdargs): """ Master function that perfoms the "update" command. """ tree=arch.tree_root() try: options, to_revision = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return from_revision = arch_compound.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return cmdutil.ensure_archive_registered(from_revision.archive) cmd=cmdutil.apply_delta(from_revision, to_revision, tree, options.patch_forward) for line in cmdutil.iter_apply_delta_filter(cmd): cmdutil.colorize(line) if to_revision.version != tree.tree_version: if cmdutil.prompt("Update version"): tree.tree_version = to_revision.version def get_parser(self): """ Returns the options parser to use for the "update" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai update [options]" " [revision/version]") parser.add_option("-f", "--forward", action="store_true", dest="patch_forward", default=False, help="pass the --forward option to 'patch'") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a revision or version is specified, that is used instead """ help_tree_spec() return class Commit(BaseCommand): """ Create a revision based on the changes in the current tree. """ def __init__(self): self.description="Write local changes to the archive" def get_completer(self, arg, index): if arg is None: arg = "" return iter_modified_file_completions(arch.tree_root(), arg) # return iter_source_file_completions(arch.tree_root(), arg) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) == 0: args = None if options.version is None: return options, tree.tree_version, args revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args def do_command(self, cmdargs): """ Master function that perfoms the "commit" command. """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) ancestor = None if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) ancestor = base else: base = ancillary.submit_revision(tree) ancestor = base if ancestor is None: ancestor = arch_compound.tree_latest(tree, version) writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) allow_old=False writethrough="implicit" if source!=None: if writethrough=="explicit" and \ cmdutil.prompt("Writethrough"): writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) elif writethrough=="none": raise CommitToMirror(archive) elif archive.is_mirror: raise CommitToMirror(archive) try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: last_revision = None if ancestor is None: if cmdutil.prompt("Import from commit"): return do_import(version) else: raise NoVersionLogs(version) try: arch_last_revision = version.iter_revisions(True).next() except StopIteration, e: arch_last_revision = None if last_revision != arch_last_revision: print "Tree is not up to date with %s" % str(version) if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: if not cmdutil.has_changed(ancestor): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise log = tree.log_message(create=False, version=version) if log is None: try: if cmdutil.prompt("Create log"): edit_log(tree, version) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) log = tree.log_message(create=False, version=version) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: if not cmdutil.prompt("Omit log summary"): raise errors.NoLogSummary try: for line in tree.iter_commit(version, seal=options.seal_version, base=base, out_of_date_ok=allow_old, file_list=files): cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "These files violate naming conventions:"): raise LintFailure(e.proc.error) else: raise def get_parser(self): """ Returns the options parser to use for the "commit" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" " [file2...]") parser.add_option("--seal", action="store_true", dest="seal_version", default=False, help="seal this version") parser.add_option("-v", "--version", dest="version", help="Use the specified version", metavar="VERSION") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") if cmdutil.supports_switch("commit", "--base"): parser.add_option("--base", dest="base", help="", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a version is specified, that is used instead """ # help_tree_spec() return class CatLog(BaseCommand): """ Print the log of a given file (from current tree) """ def __init__(self): self.description="Prints the patch log for a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "cat-log" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp() try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) log = None use_tree = (options.source == "tree" or \ (options.source == "any" and tree)) use_arch = (options.source == "archive" or options.source == "any") log = None if use_tree: for log in tree.iter_logs(revision.get_version()): if log.revision == revision: break else: log = None if log is None and use_arch: cmdutil.ensure_revision_exists(revision) log = arch.Patchlog(revision) if log is not None: for item in log.items(): print "%s: %s" % item print log.description def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai cat-log [revision]") parser.add_option("--archive", action="store_const", dest="source", const="archive", default="any", help="Always get the log from the archive") parser.add_option("--tree", action="store_const", dest="source", const="tree", help="Always get the log from the tree") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Prints the log for the specified revision """ help_tree_spec() return class Revert(BaseCommand): """ Reverts a tree (or aspects of it) to a revision """ def __init__(self): self.description="Reverts a tree (or aspects of it) to a revision " def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return iter_modified_file_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revert" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise CommandFailed(e) spec=None if options.revision is not None: spec=options.revision try: if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = ancillary.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: munger = arch_compound.MungeOpts() munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, options.hunk_prompt) if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: munger = cmdutil.arch_compound.MungeOpts(True) munger.all_types(True) if len(args) > 0: t_cwd = arch_compound.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" name = "./" + t_cwd + name munger.add_keep_file(name); if options.file_perms: munger.file_perms = True if options.file_contents: munger.file_contents = True if options.deletions: munger.deletions = True if options.additions: munger.additions = True if options.renames: munger.renames = True if options.logs: munger.add_keep_pattern('^\./\{arch\}/[^=].*') if options.control: munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ "/\.arch-inventory$") if options.pattern_files: munger.add_keep_pattern(options.pattern_files) for line in arch_compound.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") parser.add_option("", "--contents", action="store_true", dest="file_contents", help="Revert file content changes") parser.add_option("", "--permissions", action="store_true", dest="file_perms", help="Revert file permissions changes") parser.add_option("", "--deletions", action="store_true", dest="deletions", help="Restore deleted files") parser.add_option("", "--additions", action="store_true", dest="additions", help="Remove added files") parser.add_option("", "--renames", action="store_true", dest="renames", help="Revert file names") parser.add_option("--hunks", action="store_true", dest="hunk_prompt", default=False, help="Prompt which hunks to revert") parser.add_option("--pattern-files", dest="pattern_files", help="Revert files that match this pattern", metavar="REGEX") parser.add_option("--logs", action="store_true", dest="logs", default=False, help="Revert only logs") parser.add_option("--control-files", action="store_true", dest="control", default=False, help="Revert logs and other control files") parser.add_option("-n", "--no-output", action="store_true", dest="no_output", help="Don't keep an undo changeset") parser.add_option("--revision", dest="revision", help="Revert to the specified revision", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Reverts changes in the current working tree. If no flags are specified, all types of changes are reverted. Otherwise, only selected types of changes are reverted. If a revision is specified on the commandline, differences between the current tree and that revision are reverted. If a version is specified, the current tree is used to determine the revision. If files are specified, only those files listed will have any changes applied. To specify a renamed file, you can use either the old or new name. (or both!) Unless "-n" is specified, reversions can be undone with "redo". """ return class Revision(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Prints the name of a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: print str(e) return print options.display(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revision [revision]") parser.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) parser.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") parser.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") parser.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") parser.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") parser.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and prints the name of the specified revision. Instead of the name, several options can be used to print locations. If more than one is specified, the last one is used. """ help_tree_spec() return class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" self.cl_revisions = [] def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ (options, args) = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp try: self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None if options.type == "default": options.type = "archive" try: iter = cmdutil.revision_iterator(self.tree, options.type, args, options.reverse, options.modified, options.shallow) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) except cmdutil.CantDetermineVersion, e: raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) try: for revision in iter: log = None if isinstance(revision, arch.Patchlog): log = revision revision=revision.revision out = options.display(revision) if out is not None: print out if log is None and (options.summary or options.creator or options.date or options.merges): log = revision.patchlog if options.creator: print " %s" % log.creator if options.date: print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) if options.summary: print " %s" % log.summary if options.merges: showed_title = False for revision in log.merged_patches: if not showed_title: print " Merged:" showed_title = True print " %s" % revision if len(self.cl_revisions) > 0: print pylon.changelog_for_merge(self.cl_revisions) except pylon.errors.TreeRootNone: raise CommandFailedWrapper( Exception("This option can only be used in a project tree.")) def changelog_append(self, revision): if isinstance(revision, arch.Revision): revision=arch.Patchlog(revision) self.cl_revisions.append(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") cmdutil.add_revision_iter_options(select) parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", metavar="NUMBER") parser.add_option_group(select) format = cmdutil.OptionGroup(parser, "Revision format options", "These control the appearance of listed revisions") format.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) format.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") format.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") format.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") format.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") format.add_option("--changelog", action="store_const", const=self.changelog_append, dest="display", help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") display.add_option("-r", "--reverse", action="store_true", dest="reverse", help="Sort from newest to oldest") display.add_option("-s", "--summary", action="store_true", dest="summary", help="Show patchlog summary") display.add_option("-D", "--date", action="store_true", dest="date", help="Show patchlog date") display.add_option("-c", "--creator", action="store_true", dest="creator", help="Show the id that committed the" " revision") display.add_option("-m", "--merges", action="store_true", dest="merges", help="Show the revisions that were" " merged") parser.add_option_group(display) return parser def help(self, parser=None): """Attempt to explain the revisions command :param parser: If supplied, used to determine options """ if parser==None: parser=self.get_parser() parser.print_help() print """List revisions. """ help_tree_spec() class Get(BaseCommand): """ Retrieve a revision from the archive """ def __init__(self): self.description="Retrieve a revision from the archive" self.parser=self.get_parser() def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "get" command. """ (options, args) = self.parser.parse_args(cmdargs) if len(args) < 1: return self.help() try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None arch_loc = None try: revision, arch_loc = paths.full_path_decode(args[0]) except Exception, e: revision = cmdutil.determine_revision_arch(tree, args[0], check_existence=False, allow_package=True) if len(args) > 1: directory = args[1] else: directory = str(revision.nonarch) if os.path.exists(directory): raise DirectoryExists(directory) cmdutil.ensure_archive_registered(revision.archive, arch_loc) try: cmdutil.ensure_revision_exists(revision) except cmdutil.NoSuchRevision, e: raise CommandFailedWrapper(e) link = cmdutil.prompt ("get link") for line in cmdutil.iter_get(revision, directory, link, options.no_pristine, options.no_greedy_add): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "get" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai get revision [dir]") parser.add_option("--no-pristine", action="store_true", dest="no_pristine", help="Do not make pristine copy for reference") parser.add_option("--no-greedy-add", action="store_true", dest="no_greedy_add", help="Never add to greedy libraries") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and constructs a project tree for a revision. If the optional "dir" argument is provided, the project tree will be stored in this directory. """ help_tree_spec() return class PromptCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.prompt = "Fai> " try: self.tree = arch.tree_root() except: self.tree = None self.set_title() self.set_prompt() self.fake_aba = abacmds.AbaCmds() self.identchars += '-' self.history_file = os.path.expanduser("~/.fai-history") readline.set_completer_delims(string.whitespace) if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) self.cwd = os.getcwd() def write_history(self): readline.write_history_file(self.history_file) def do_quit(self, args): self.write_history() sys.exit(0) def do_exit(self, args): self.do_quit(args) def do_EOF(self, args): print self.do_quit(args) def postcmd(self, line, bar): self.set_title() self.set_prompt() def set_prompt(self): if self.tree is not None: try: prompt = pylon.alias_or_version(self.tree.tree_version, self.tree, full=False) if prompt is not None: prompt = " " + prompt except: prompt = "" else: prompt = "" self.prompt = "Fai%s> " % prompt def set_title(self, command=None): try: version = pylon.alias_or_version(self.tree.tree_version, self.tree, full=False) except: version = "[no version]" if command is None: command = "" sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) def do_cd(self, line): if line == "": line = "~" line = os.path.expanduser(line) if os.path.isabs(line): newcwd = line else: newcwd = self.cwd+'/'+line newcwd = os.path.normpath(newcwd) try: os.chdir(newcwd) self.cwd = newcwd except Exception, e: print e try: self.tree = arch.tree_root() except: self.tree = None def do_help(self, line): Help()(line) def default(self, line): args = line.split() if find_command(args[0]): try: find_command(args[0]).do_command(args[1:]) except cmdutil.BadCommandOption, e: print e except cmdutil.GetHelp, e: find_command(args[0]).help() except CommandFailed, e: print e except arch.errors.ArchiveNotRegistered, e: print e except KeyboardInterrupt, e: print "Interrupted" except arch.util.ExecProblem, e: print e.proc.error.rstrip('\n') except cmdutil.CantDetermineVersion, e: print e except cmdutil.CantDetermineRevision, e: print e except Exception, e: print "Unhandled error:\n%s" % errors.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] elif self.fake_aba.is_command(args[0]): tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass cmd = self.fake_aba.is_command(args[0]) try: cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) except KeyboardInterrupt, e: print "Interrupted" elif options.tla_fallthrough and args[0] != "rm" and \ cmdutil.is_tla_command(args[0]): try: tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass args = cmdutil.expand_prefix_alias(args, tree) arch.util.exec_safe('tla', args, stderr=sys.stderr, expected=(0, 1)) except arch.util.ExecProblem, e: pass except KeyboardInterrupt, e: print "Interrupted" else: try: try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None args=line.split() os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) except KeyboardInterrupt, e: print "Interrupted" def completenames(self, text, line, begidx, endidx): completions = [] iter = iter_command_names(self.fake_aba) try: if len(line) > 0: arg = line.split()[-1] else: arg = "" iter = cmdutil.iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) def completedefault(self, text, line, begidx, endidx): """Perform completion for native commands. :param text: The text to complete :type text: str :param line: The entire line to complete :type line: str :param begidx: The start of the text in the line :type begidx: int :param endidx: The end of the text in the line :type endidx: int """ try: (cmd, args, foo) = self.parseline(line) command_obj=find_command(cmd) if command_obj is not None: return command_obj.complete(args.split(), text) elif not self.fake_aba.is_command(cmd) and \ cmdutil.is_tla_command(cmd): iter = cmdutil.iter_supported_switches(cmd) if len(args) > 0: arg = args.split()[-1] else: arg = "" if arg.startswith("-"): return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return list(cmdutil.iter_munged_completions( cmdutil.iter_file_completions(arg), arg, text)) elif cmd == "cd": if len(args) > 0: arg = args.split()[-1] else: arg = "" iter = cmdutil.iter_dir_completions(arg) iter = cmdutil.iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] iter = cmdutil.iter_file_completions(arg) return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: print e def iter_command_names(fake_aba): for entry in cmdutil.iter_combine([commands.iterkeys(), fake_aba.get_commands(), cmdutil.iter_tla_commands(False)]): if not suggestions.has_key(str(entry)): yield entry def iter_source_file_completions(tree, arg): treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in tree.iter_inventory(dirs, source=True, both=True): file = file_completion_match(file, treepath, arg) if file is not None: yield file def iter_untagged(tree, dirs): for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, categories=arch_core.non_root, control_files=True): yield file.name def iter_untagged_completions(tree, arg): """Generate an iterator for all visible untagged files that match arg. :param tree: The tree to look for untagged files in :type tree: `arch.WorkingTree` :param arg: The argument to match :type arg: str :return: An iterator of all matching untagged files :rtype: iterator of str """ treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in iter_untagged(tree, dirs): file = file_completion_match(file, treepath, arg) if file is not None: yield file def file_completion_match(file, treepath, arg): """Determines whether a file within an arch tree matches the argument. :param file: The rooted filename :type file: str :param treepath: The path to the cwd within the tree :type treepath: str :param arg: The prefix to match :return: The completion name, or None if not a match :rtype: str """ if not file.startswith(treepath): return None if treepath != "": file = file[len(treepath)+1:] if not file.startswith(arg): return None if os.path.isdir(file): file += '/' return file def iter_modified_file_completions(tree, arg): """Returns a list of modified files that match the specified prefix. :param tree: The current tree :type tree: `arch.WorkingTree` :param arg: The prefix to match :type arg: str """ treepath = arch_compound.tree_cwd(tree) tmpdir = util.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) for line in arch.iter_delta(revision, tree, changeset): if isinstance(line, arch.FileModification): file = file_completion_match(line.name[1:], treepath, arg) if file is not None: completions.append(file) shutil.rmtree(tmpdir) return completions class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" def do_command(self, cmdargs): if len(cmdargs)!=0: raise cmdutil.GetHelp prompt = PromptCmd() try: prompt.cmdloop() finally: prompt.write_history() class AddID(BaseCommand): """ Adds an inventory id for the given file """ def __init__(self): self.description="Add an inventory id for a given file" def get_completer(self, arg, index): tree = arch.tree_root() return iter_untagged_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise pylon.errors.CommandFailedWrapper(e) if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp #if options.id and len(args) != 1: # print "If --id is specified, only one file can be named." # return method = tree.tagging_method if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): if method == "explicit" or method == "implicit": options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "implicit": if method != "implicit": if not cmdutil.prompt("Implicit in other tree"): if method == "explicit" or method == "tagline": options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): print "add-id not supported for \"%s\" tagging method" % \ method return if options.id_type == "auto": if method != "tagline" and method != "explicit" \ and method !="implicit": print "add-id not supported for \"%s\" tagging method" % method return else: options.id_type = method if options.untagged: args = None self.add_ids(tree, options.id_type, args) def add_ids(self, tree, id_type, files=()): """Add inventory ids to files. :param tree: the tree the files are in :type tree: `arch.WorkingTree` :param id_type: the type of id to add: "explicit" or "tagline" :type id_type: str :param files: The list of files to add. If None do all untagged. :type files: tuple of str """ untagged = (files is None) if untagged: files = list(iter_untagged(tree, None)) previous_files = [] while len(files) > 0: previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) elif id_type == "tagline" or id_type == "implicit": for file in files: try: implicit = (id_type == "implicit") cmdutil.add_tagline_or_explicit_id(file, False, implicit) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: pass #do inventory after tagging until no untagged files are encountered if untagged: files = [] for file in iter_untagged(tree, None): if not file in previous_files: files.append(file) else: break def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") # ddaa suggests removing this to promote GUIDs. Let's see who squalks. # parser.add_option("-i", "--id", dest="id", # help="Specify id for a single file", default=None) parser.add_option("--tltl", action="store_true", dest="lord_style", help="Use Tom Lord's style of id.") parser.add_option("--explicit", action="store_const", const="explicit", dest="id_type", help="Use an explicit id", default="auto") parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") parser.add_option("--implicit", action="store_const", const="implicit", dest="id_type", help="Use an implicit id (deprecated)") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Adds an inventory to the specified file(s) and directories. If --untagged is specified, adds inventory to all untagged files and directories. """ return class Merge(BaseCommand): """ Merges changes from other versions into the current tree """ def __init__(self): self.description="Merges changes from other versions" try: self.tree = arch.tree_root() except: self.tree = None def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError return cmdutil.merge_completions(self.tree, arg, index) def do_command(self, cmdargs): """ Master function that perfoms the "merge" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if options.diff3: action="star-merge" else: action = options.action if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) if cmdutil.has_changed(ancillary.comp_revision(self.tree)): raise UncommittedChanges(self.tree) if len(args) > 0: revisions = [] for arg in args: revisions.append(cmdutil.determine_revision_arch(self.tree, arg)) source = "from commandline" else: revisions = ancillary.iter_partner_revisions(self.tree, self.tree.tree_version) source = "from partner version" revisions = misc.rewind_iterator(revisions) try: revisions.next() revisions.rewind() except StopIteration, e: revision = cmdutil.tag_cur(self.tree) if revision is None: raise CantDetermineRevision("", "No version specified, no " "partner-versions, and no tag" " source") revisions = [revision] source = "from tag source" for revision in revisions: cmdutil.ensure_archive_registered(revision.archive) cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % (revision, source))) if action=="native-merge" or action=="update": if self.native_merge(revision, action) == 0: continue elif action=="star-merge": try: self.star_merge(revision, options.diff3) except errors.MergeProblem, e: break if cmdutil.has_changed(self.tree.tree_version): break def star_merge(self, revision, diff3): """Perform a star-merge on the current tree. :param revision: The revision to use for the merge :type revision: `arch.Revision` :param diff3: If true, do a diff3 merge :type diff3: bool """ try: for line in self.tree.iter_star_merge(revision, diff3=diff3): cmdutil.colorize(line) except arch.util.ExecProblem, e: if e.proc.status is not None and e.proc.status == 1: if e.proc.error: print e.proc.error raise MergeProblem else: raise def native_merge(self, other_revision, action): """Perform a native-merge on the current tree. :param other_revision: The revision to use for the merge :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ other_tree = arch_compound.find_or_make_local_revision(other_revision) try: if action == "native-merge": ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, other_revision) elif action == "update": ancestor = arch_compound.tree_latest(self.tree, other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) if (ancestor == other_revision): cmdutil.colorize(arch.Chatter("* Skipping redundant merge" % ancestor)) return 0 delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line) return 1 def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai merge [VERSION]") parser.add_option("-s", "--star-merge", action="store_const", dest="action", help="Use star-merge", const="star-merge", default="native-merge") parser.add_option("--update", action="store_const", dest="action", help="Use update picker", const="update") parser.add_option("--diff3", action="store_true", dest="diff3", help="Use diff3 for merge (implies star-merge)") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Performs a merge operation using the specified version. """ return class ELog(BaseCommand): """ Produces a raw patchlog and invokes the user's editor """ def __init__(self): self.description="Edit a patchlog to commit" try: self.tree = arch.tree_root() except: self.tree = None def do_command(self, cmdargs): """ Master function that perfoms the "elog" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if self.tree is None: raise arch.errors.TreeRootError try: edit_log(self.tree, self.tree.tree_version) except pylon.errors.NoEditorSpecified, e: raise pylon.errors.CommandFailedWrapper(e) def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai elog") return parser def help(self, parser=None): """ Invokes $EDITOR to produce a log for committing. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Invokes $EDITOR to produce a log for committing. """ return def edit_log(tree, version): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge :param tree: The tree to edit the log for :type tree: `arch.WorkingTree` """ #ensure we have an editor before preparing the log cmdutil.find_editor() log = tree.log_message(create=False, version=version) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) log = tree.log_message(create=True, version=version) log_is_new = True tmplog = log.name template = pylon.log_template_path(tree) if template: shutil.copyfile(template, tmplog) comp_version = ancillary.comp_revision(tree).version new_merges = cmdutil.iter_new_merges(tree, comp_version) new_merges = cmdutil.direct_merges(new_merges) log["Summary"] = pylon.merge_summary(new_merges, version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): if cmdutil.prompt("changelog for merge"): mergestuff = "Patches applied:\n" mergestuff += pylon.changelog_for_merge(new_merges) else: mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: cmdutil.invoke_editor(log.name) except: if log_is_new: os.remove(log.name) raise class MirrorArchive(BaseCommand): """ Updates a mirror from an archive """ def __init__(self): self.description="Update a mirror from an archive" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if len(args) > 1: raise GetHelp try: tree = arch.tree_root() except: tree = None if len(args) == 0: if tree is not None: name = tree.tree_version() else: name = cmdutil.expand_alias(args[0], tree) name = arch.NameParser(name) to_arch = name.get_archive() from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) limit = name.get_nonarch() iter = arch_core.mirror_archive(from_arch,to_arch, limit) for line in arch.chatter_classifier(iter): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Updates a mirror from an archive. If a branch, package, or version is supplied, only changes under it are mirrored. """ return def help_tree_spec(): print """Specifying revisions (default: tree) Revisions may be specified by alias, revision, version or patchlevel. Revisions or versions may be fully qualified. Unqualified revisions, versions, or patchlevels use the archive of the current project tree. Versions will use the latest patchlevel in the tree. Patchlevels will use the current tree- version. Use "alias" to list available (user and automatic) aliases.""" auto_alias = [ "acur", "The latest revision in the archive of the tree-version. You can specify \ a different version like so: acur:foo--bar--0 (aliases can be used)", "tcur", """(tree current) The latest revision in the tree of the tree-version. \ You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ used).""", "tprev" , """(tree previous) The previous revision in the tree of the tree-version. To \ specify an older revision, use a number, e.g. "tprev:4" """, "tanc" , """(tree ancestor) The ancestor revision of the tree To specify an older \ revision, use a number, e.g. "tanc:4".""", "tdate" , """(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", "tmod" , """ (tree modified) The latest revision to modify a given file, e.g. \ "tmod:engine.cpp" or "tmod:engine.cpp:16".""", "ttag" , """(tree tag) The revision that was tagged into the current tree revision, \ according to the tree""", "tagcur", """(tag current) The latest revision of the version that the current tree \ was tagged from.""", "mergeanc" , """The common ancestor of the current tree and the specified revision. \ Defaults to the first partner-version's latest revision or to tagcur.""", ] def is_auto_alias(name): """Determine whether a name is an auto alias name :param name: the name to check :type name: str :return: True if the name is an auto alias, false if not :rtype: bool """ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] def display_def(iter, wrap = 80): """Display a list of definitions :param iter: iter of name, definition pairs :type iter: iter of (str, str) :param wrap: The width for text wrapping :type wrap: int """ vals = list(iter) maxlen = 0 for (key, value) in vals: if len(key) > maxlen: maxlen = len(key) for (key, value) in vals: tw=textwrap.TextWrapper(width=wrap, initial_indent=key.rjust(maxlen)+" : ", subsequent_indent="".rjust(maxlen+3)) print tw.fill(value) def help_aliases(tree): print """Auto-generated aliases""" display_def(pylon.util.iter_pairs(auto_alias)) print "User aliases" display_def(ancillary.iter_all_alias(tree)) class Inventory(BaseCommand): """List the status of files in the tree""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() categories = [] if (options.source): categories.append(arch_core.SourceFile) if (options.precious): categories.append(arch_core.PreciousFile) if (options.backup): categories.append(arch_core.BackupFile) if (options.junk): categories.append(arch_core.JunkFile) if len(categories) == 1: show_leading = False else: show_leading = True if len(categories) == 0: categories = None if options.untagged: categories = arch_core.non_root show_leading = False tagged = False else: tagged = None for file in arch_core.iter_inventory_filter(tree, None, control_files=options.control_files, categories = categories, tagged=tagged): print arch_core.file_line(file, category = show_leading, untagged = show_leading, id = options.ids) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai inventory [options]") parser.add_option("--ids", action="store_true", dest="ids", help="Show file ids") parser.add_option("--control", action="store_true", dest="control_files", help="include control files") parser.add_option("--source", action="store_true", dest="source", help="List source files") parser.add_option("--backup", action="store_true", dest="backup", help="List backup files") parser.add_option("--precious", action="store_true", dest="precious", help="List precious files") parser.add_option("--junk", action="store_true", dest="junk", help="List junk files") parser.add_option("--unrecognized", action="store_true", dest="unrecognized", help="List unrecognized files") parser.add_option("--untagged", action="store_true", dest="untagged", help="List only untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists the status of files in the archive: S source P precious B backup J junk U unrecognized T tree root ? untagged-source Leading letter are not displayed if only one kind of file is shown """ return class Alias(BaseCommand): """List or adjust aliases""" def __init__(self): self.description=self.__doc__ def get_completer(self, arg, index): if index > 2: return () try: self.tree = arch.tree_root() except: self.tree = None if index == 0: return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] elif index == 1: return cmdutil.iter_revision_completions(arg, self.tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree = arch.tree_root() except: self.tree = None try: options.action(args, options) except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) def no_prefix(self, alias): if alias.startswith("^"): alias = alias[1:] return alias def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments :param args: The list of commandline arguments :type args: list of str :param options: The commandline options """ if len(args) == 0: help_aliases(self.tree) return else: alias = self.no_prefix(args[0]) if len(args) == 1: self.print_alias(alias) elif (len(args)) == 2: self.add(alias, args[1], options) else: raise cmdutil.GetHelp def print_alias(self, alias): answer = None if is_auto_alias(alias): raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." " Use \"revision\" to expand auto aliases." % alias) for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] if answer is not None: print answer else: print "The alias %s is not assigned." % alias def add(self, alias, expansion, options): """Add or modify aliases :param alias: The alias name to create/modify :type alias: str :param expansion: The expansion to assign to the alias name :type expansion: str :param options: The commandline options """ if is_auto_alias(alias): raise IsAutoAlias(alias) newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, self.tree)) ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) elif not written: newlist+=new_line written = True if not written: newlist+=new_line self.write_aliases(newlist, options) def delete(self, args, options): """Delete the specified alias :param args: The list of arguments :type args: list of str :param options: The commandline options """ deleted = False if len(args) != 1: raise cmdutil.GetHelp alias = self.no_prefix(args[0]) if is_auto_alias(alias): raise IsAutoAlias(alias) newlist = "" for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: raise errors.NoSuchAlias(alias) self.write_aliases(newlist, options) def get_alias_file(self, options): """Return the name of the alias file to use :param options: The commandline options """ if options.tree: if self.tree is None: self.tree == arch.tree_root() return str(self.tree)+"/{arch}/+aliases" else: return "~/.aba/aliases" def get_iterator(self, options): """Return the alias iterator to use :param options: The commandline options """ return ancillary.iter_alias(self.get_alias_file(options)) def write_aliases(self, newlist, options): """Safely rewrite the alias file :param newlist: The new list of aliases :type newlist: str :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) file = util.NewFileVersion(filename) file.write(newlist) file.commit() def get_parser(self): """ Returns the options parser to use for the "alias" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") parser.add_option("-d", "--delete", action="store_const", dest="action", const=self.delete, default=self.arg_dispatch, help="Delete an alias") parser.add_option("--tree", action="store_true", dest="tree", help="Create a per-tree alias", default=False) return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists current aliases or modifies the list of aliases. If no arguments are supplied, aliases will be listed. If two arguments are supplied, the specified alias will be created or modified. If -d or --delete is supplied, the specified alias will be deleted. You can create aliases that refer to any fully-qualified part of the Arch namespace, e.g. archive, archive/category, archive/category--branch, archive/category--branch--version (my favourite) archive/category--branch--version--patchlevel Aliases can be used automatically by native commands. To use them with external or tla commands, prefix them with ^ (you can do this with native commands, too). """ class RequestMerge(BaseCommand): """Submit a merge request to Bug Goo""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """Submit a merge request :param cmdargs: The commandline arguments :type cmdargs: list of str """ parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: cmdutil.find_editor() except pylon.errors.NoEditorSpecified, e: raise pylon.errors.CommandFailedWrapper(e) try: self.tree=arch.tree_root() except: self.tree=None base, revisions = self.revision_specs(args) message = self.make_headers(base, revisions) message += self.make_summary(revisions) path = self.edit_message(message) message = self.tidy_message(path) if cmdutil.prompt("Send merge"): self.send_message(message) print "Merge request sent" def make_headers(self, base, revisions): """Produce email and Bug Goo header strings :param base: The base revision to apply merges to :type base: `arch.Revision` :param revisions: The revisions to replay into the base :type revisions: list of `arch.Patchlog` :return: The headers :rtype: str """ headers = "To: gnu-arch-users@gnu.org\n" headers += "From: %s\n" % options.fromaddr if len(revisions) == 1: headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary else: headers += "Subject: [MERGE REQUEST]\n" headers += "\n" headers += "Base-Revision: %s\n" % base for revision in revisions: headers += "Revision: %s\n" % revision.revision headers += "Bug: \n\n" return headers def make_summary(self, logs): """Generate a summary of merges :param logs: the patchlogs that were directly added by the merges :type logs: list of `arch.Patchlog` :return: the summary :rtype: str """ summary = "" for log in logs: summary+=str(log.revision)+"\n" summary+=log.summary+"\n" if log.description.strip(): summary+=log.description.strip('\n')+"\n\n" return summary def revision_specs(self, args): """Determine the base and merge revisions from tree and arguments. :param args: The parsed arguments :type args: list of str :return: The base revision and merge revisions :rtype: `arch.Revision`, list of `arch.Patchlog` """ if len(args) > 0: target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: target_revision = arch_compound.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] else: if self.tree is None: raise CantDetermineRevision("", "Not in a project tree") merge_iter = cmdutil.iter_new_merges(self.tree, target_revision.version, False) merges = [f for f in cmdutil.direct_merges(merge_iter)] return (target_revision, merges) def edit_message(self, message): """Edit an email message in the user's standard editor :param message: The message to edit :type message: str :return: the path of the edited message :rtype: str """ if self.tree is None: path = os.get_cwd() else: path = self.tree path += "/,merge-request" file = open(path, 'w') file.write(message) file.flush() cmdutil.invoke_editor(path) return path def tidy_message(self, path): """Validate and clean up message. :param path: The path to the message to clean up :type path: str :return: The parsed message :rtype: `email.Message` """ mail = email.message_from_file(open(path)) if mail["Subject"].strip() == "[MERGE REQUEST]": raise BlandSubject request = email.message_from_string(mail.get_payload()) if request.has_key("Bug"): if request["Bug"].strip()=="": del request["Bug"] mail.set_payload(request.as_string()) return mail def send_message(self, message): """Send a message, using its headers to address it. :param message: The message to send :type message: `email.Message`""" server = smtplib.SMTP("localhost") server.sendmail(message['From'], message['To'], message.as_string()) server.quit() def help(self, parser=None): """Print a usage message :param parser: The options parser to use :type parser: `cmdutil.CmdOptionParser` """ if parser is None: parser = self.get_parser() parser.print_help() print """ Sends a merge request formatted for Bug Goo. Intended use: get the tree you'd like to merge into. Apply the merges you want. Invoke request-merge. The merge request will open in your $EDITOR. When no TARGET is specified, it uses the current tree revision. When no MERGE is specified, it uses the direct merges (as in "revisions --direct-merges"). But you can specify just the TARGET, or all the MERGE revisions. """ def get_parser(self): """Produce a commandline parser for this command. :rtype: `cmdutil.CmdOptionParser` """ parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") return parser commands = { 'changes' : Changes, 'help' : Help, 'update': Update, 'apply-changes':ApplyChanges, 'cat-log': CatLog, 'commit': Commit, 'revision': Revision, 'revisions': Revisions, 'get': Get, 'revert': Revert, 'shell': Shell, 'add-id': AddID, 'merge': Merge, 'elog': ELog, 'mirror-archive': MirrorArchive, 'ninventory': Inventory, 'alias' : Alias, 'request-merge': RequestMerge, } def my_import(mod_name): module = __import__(mod_name) components = mod_name.split('.') for comp in components[1:]: module = getattr(module, comp) return module def plugin(mod_name): module = my_import(mod_name) module.add_command(commands) for file in os.listdir(sys.path[0]+"/command"): if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": plugin("command."+file[:-3]) suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", 'diff-rev' : "To compare two revisions, use \"changes\".", 'undo' : "To undo local changes, use \"revert\".", 'undelete' : "To undo only deletions, use \"revert --deletions\"", 'missing-from' : "Try \"revisions --missing-from\".", 'missing' : "Try \"revisions --missing\".", 'missing-merge' : "Try \"revisions --partner-missing\".", 'new-merges' : "Try \"revisions --new-merges\".", 'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", 'logs' : "Try \"revisions --logs\"", 'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", 'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", 'change-version' : "Try \"update REVISION\"", 'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", 'rev-depends' : "Use revisions --dependencies", 'auto-get' : "Plain get will do archive lookups", 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", 'file-revert' : "Use revert FILE", 'join-branch' : "Use replay --logs-only" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.2.1/test_patches_data/mod-2000064400000000000000000000441611046102023000157610ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/mod-3000064400000000000000000000442031046102023000157570ustar 00000000000000First line change # Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/mod-4000064400000000000000000000440421046102023000157610ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() last line change patchkit-0.2.1/test_patches_data/mod-5000064400000000000000000000331241046102023000157610ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/mod-6000064400000000000000000000000261046102023000157550ustar 00000000000000Total contents change patchkit-0.2.1/test_patches_data/mod-7000064400000000000000000000000211046102023000157510ustar 00000000000000No newline eitherpatchkit-0.2.1/test_patches_data/orig000064400000000000000000003036341046102023000160060ustar 00000000000000# Copyright (C) 2004 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import sys import arch import arch.util import arch.arch import abacmds import cmdutil import shutil import os import options import paths import time import cmd import readline import re import string import arch_core from errors import * import errors import terminal import ancillary import misc import email import smtplib __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" commands = {} def find_command(cmd): """ Return an instance of a command type. Return None if the type isn't registered. :param cmd: the name of the command to look for :type cmd: the type of the command """ if commands.has_key(cmd): return commands[cmd]() else: return None class BaseCommand: def __call__(self, cmdline): try: self.do_command(cmdline.split()) except cmdutil.GetHelp, e: self.help() except Exception, e: print e def get_completer(index): return None def complete(self, args, text): """ Returns a list of possible completions for the given text. :param args: The complete list of arguments :type args: List of str :param text: text to complete (may be shorter than args[-1]) :type text: str :rtype: list of str """ matches = [] candidates = None if len(args) > 0: realtext = args[-1] else: realtext = "" try: parser=self.get_parser() if realtext.startswith('-'): candidates = parser.iter_options() else: (options, parsed_args) = parser.parse_args(args) if len (parsed_args) > 0: candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) else: candidates = self.get_completer("", 0) except: pass if candidates is None: return for candidate in candidates: candidate = str(candidate) if candidate.startswith(realtext): matches.append(candidate[len(realtext)- len(text):]) return matches class Help(BaseCommand): """ Lists commands, prints help messages. """ def __init__(self): self.description="Prints help mesages" self.parser = None def do_command(self, cmdargs): """ Prints a help message. """ options, args = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp if options.native or options.suggestions or options.external: native = options.native suggestions = options.suggestions external = options.external else: native = True suggestions = False external = True if len(args) == 0: self.list_commands(native, suggestions, external) return elif len(args) == 1: command_help(args[0]) return def help(self): self.get_parser().print_help() print """ If no command is specified, commands are listed. If a command is specified, help for that command is listed. """ def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ if self.parser is not None: return self.parser parser=cmdutil.CmdOptionParser("fai help [command]") parser.add_option("-n", "--native", action="store_true", dest="native", help="Show native commands") parser.add_option("-e", "--external", action="store_true", dest="external", help="Show external commands") parser.add_option("-s", "--suggest", action="store_true", dest="suggestions", help="Show suggestions") self.parser = parser return parser def list_commands(self, native=True, suggest=False, external=True): """ Lists supported commands. :param native: list native, python-based commands :type native: bool :param external: list external aba-style commands :type external: bool """ if native: print "Native Fai commands" keys=commands.keys() keys.sort() for k in keys: space="" for i in range(28-len(k)): space+=" " print space+k+" : "+commands[k]().description print if suggest: print "Unavailable commands and suggested alternatives" key_list = suggestions.keys() key_list.sort() for key in key_list: print "%28s : %s" % (key, suggestions[key]) print if external: fake_aba = abacmds.AbaCmds() if (fake_aba.abadir == ""): return print "External commands" fake_aba.list_commands() print if not suggest: print "Use help --suggest to list alternatives to tla and aba"\ " commands." if options.tla_fallthrough and (native or external): print "Fai also supports tla commands." def command_help(cmd): """ Prints help for a command. :param cmd: The name of the command to print help for :type cmd: str """ fake_aba = abacmds.AbaCmds() cmdobj = find_command(cmd) if cmdobj != None: cmdobj.help() elif suggestions.has_key(cmd): print "Not available\n" + suggestions[cmd] else: abacmd = fake_aba.is_command(cmd) if abacmd: abacmd.help() else: print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" class Changes(BaseCommand): """ the "changes" command: lists differences between trees/revisions: """ def __init__(self): self.description="Lists what files have changed in the project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp tree=arch.tree_root() if len(args) == 0: a_spec = cmdutil.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) if len(args) == 2: b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) else: b_spec=tree return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that perfoms the "changes" command. """ try: options, a_spec, b_spec = self.parse_commandline(cmdargs); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return if options.changeset: changeset=options.changeset tmpdir = None else: tmpdir=cmdutil.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) try: for line in delta: if cmdutil.chattermatch(line, "changeset:"): pass else: cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise status=delta.status if status > 1: return if (options.perform_diff): chan = cmdutil.ChangesetMunger(changeset) chan.read_indices() if isinstance(b_spec, arch.Revision): b_dir = b_spec.library_find() else: b_dir = b_spec a_dir = a_spec.library_find() if options.diffopts is not None: diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: cmdutil.show_diffs(delta.changeset) finally: if tmpdir and (os.access(tmpdir, os.X_OK)): shutil.rmtree(tmpdir) def get_parser(self): """ Returns the options parser to use for the "changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" " [revision]") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") parser.add_option("--diffopts", dest="diffopts", help="Use the specified diff options", metavar="OPTIONS") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Performs source-tree comparisons If no revision is specified, the current project tree is compared to the last-committed revision. If one revision is specified, the current project tree is compared to that revision. If two revisions are specified, they are compared to each other. """ help_tree_spec() return class ApplyChanges(BaseCommand): """ Apply differences between two revisions to a tree """ def __init__(self): self.description="Applies changes to a project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) != 2: raise cmdutil.GetHelp a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that performs "apply-changes". """ try: tree = arch.tree_root() options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return delta=cmdutil.apply_delta(a_spec, b_spec, tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line, options.suppress_chatter) def get_parser(self): """ Returns the options parser to use for the "apply-changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" " revision") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Applies changes to a project tree Compares two revisions and applies the difference between them to the current tree. """ help_tree_spec() return class Update(BaseCommand): """ Updates a project tree to a given revision, preserving un-committed hanges. """ def __init__(self): self.description="Apply the latest changes to the current directory" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp spec=None if len(args)>0: spec=args[0] revision=cmdutil.determine_revision_arch(tree, spec) cmdutil.ensure_archive_registered(revision.archive) mirror_source = cmdutil.get_mirror_source(revision.archive) if mirror_source != None: if cmdutil.prompt("Mirror update"): cmd=cmdutil.mirror_archive(mirror_source, revision.archive, arch.NameParser(revision).get_package_version()) for line in arch.chatter_classifier(cmd): cmdutil.colorize(line, options.suppress_chatter) revision=cmdutil.determine_revision_arch(tree, spec) return options, revision def do_command(self, cmdargs): """ Master function that perfoms the "update" command. """ tree=arch.tree_root() try: options, to_revision = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return from_revision=cmdutil.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return cmdutil.ensure_archive_registered(from_revision.archive) cmd=cmdutil.apply_delta(from_revision, to_revision, tree, options.patch_forward) for line in cmdutil.iter_apply_delta_filter(cmd): cmdutil.colorize(line) if to_revision.version != tree.tree_version: if cmdutil.prompt("Update version"): tree.tree_version = to_revision.version def get_parser(self): """ Returns the options parser to use for the "update" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai update [options]" " [revision/version]") parser.add_option("-f", "--forward", action="store_true", dest="patch_forward", default=False, help="pass the --forward option to 'patch'") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a revision or version is specified, that is used instead """ help_tree_spec() return class Commit(BaseCommand): """ Create a revision based on the changes in the current tree. """ def __init__(self): self.description="Write local changes to the archive" def get_completer(self, arg, index): if arg is None: arg = "" return iter_modified_file_completions(arch.tree_root(), arg) # return iter_source_file_completions(arch.tree_root(), arg) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) == 0: args = None revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args def do_command(self, cmdargs): """ Master function that perfoms the "commit" command. """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) else: base = cmdutil.submit_revision(tree) writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) allow_old=False writethrough="implicit" if source!=None: if writethrough=="explicit" and \ cmdutil.prompt("Writethrough"): writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) elif writethrough=="none": raise CommitToMirror(archive) elif archive.is_mirror: raise CommitToMirror(archive) try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: if cmdutil.prompt("Import from commit"): return do_import(version) else: raise NoVersionLogs(version) if last_revision!=version.iter_revisions(True).next(): if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: if not cmdutil.has_changed(version): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise log = tree.log_message(create=False) if log is None: try: if cmdutil.prompt("Create log"): edit_log(tree) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) log = tree.log_message(create=False) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: if not cmdutil.prompt("Omit log summary"): raise errors.NoLogSummary try: for line in tree.iter_commit(version, seal=options.seal_version, base=base, out_of_date_ok=allow_old, file_list=files): cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "These files violate naming conventions:"): raise LintFailure(e.proc.error) else: raise def get_parser(self): """ Returns the options parser to use for the "commit" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" " [file2...]") parser.add_option("--seal", action="store_true", dest="seal_version", default=False, help="seal this version") parser.add_option("-v", "--version", dest="version", help="Use the specified version", metavar="VERSION") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") if cmdutil.supports_switch("commit", "--base"): parser.add_option("--base", dest="base", help="", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a version is specified, that is used instead """ # help_tree_spec() return class CatLog(BaseCommand): """ Print the log of a given file (from current tree) """ def __init__(self): self.description="Prints the patch log for a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "cat-log" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp() try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) log = None use_tree = (options.source == "tree" or \ (options.source == "any" and tree)) use_arch = (options.source == "archive" or options.source == "any") log = None if use_tree: for log in tree.iter_logs(revision.get_version()): if log.revision == revision: break else: log = None if log is None and use_arch: cmdutil.ensure_revision_exists(revision) log = arch.Patchlog(revision) if log is not None: for item in log.items(): print "%s: %s" % item print log.description def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai cat-log [revision]") parser.add_option("--archive", action="store_const", dest="source", const="archive", default="any", help="Always get the log from the archive") parser.add_option("--tree", action="store_const", dest="source", const="tree", help="Always get the log from the tree") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Prints the log for the specified revision """ help_tree_spec() return class Revert(BaseCommand): """ Reverts a tree (or aspects of it) to a revision """ def __init__(self): self.description="Reverts a tree (or aspects of it) to a revision " def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return iter_modified_file_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revert" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise CommandFailed(e) spec=None if options.revision is not None: spec=options.revision try: if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: munger = cmdutil.MungeOpts() munger.hunk_prompt = options.hunk_prompt if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: munger = cmdutil.MungeOpts(True) munger.all_types(True) if len(args) > 0: t_cwd = cmdutil.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" name = "./" + t_cwd + name munger.add_keep_file(name); if options.file_perms: munger.file_perms = True if options.file_contents: munger.file_contents = True if options.deletions: munger.deletions = True if options.additions: munger.additions = True if options.renames: munger.renames = True if options.logs: munger.add_keep_pattern('^\./\{arch\}/[^=].*') if options.control: munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ "/\.arch-inventory$") if options.pattern_files: munger.add_keep_pattern(options.pattern_files) for line in cmdutil.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") parser.add_option("", "--contents", action="store_true", dest="file_contents", help="Revert file content changes") parser.add_option("", "--permissions", action="store_true", dest="file_perms", help="Revert file permissions changes") parser.add_option("", "--deletions", action="store_true", dest="deletions", help="Restore deleted files") parser.add_option("", "--additions", action="store_true", dest="additions", help="Remove added files") parser.add_option("", "--renames", action="store_true", dest="renames", help="Revert file names") parser.add_option("--hunks", action="store_true", dest="hunk_prompt", default=False, help="Prompt which hunks to revert") parser.add_option("--pattern-files", dest="pattern_files", help="Revert files that match this pattern", metavar="REGEX") parser.add_option("--logs", action="store_true", dest="logs", default=False, help="Revert only logs") parser.add_option("--control-files", action="store_true", dest="control", default=False, help="Revert logs and other control files") parser.add_option("-n", "--no-output", action="store_true", dest="no_output", help="Don't keep an undo changeset") parser.add_option("--revision", dest="revision", help="Revert to the specified revision", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Reverts changes in the current working tree. If no flags are specified, all types of changes are reverted. Otherwise, only selected types of changes are reverted. If a revision is specified on the commandline, differences between the current tree and that revision are reverted. If a version is specified, the current tree is used to determine the revision. If files are specified, only those files listed will have any changes applied. To specify a renamed file, you can use either the old or new name. (or both!) Unless "-n" is specified, reversions can be undone with "redo". """ return class Revision(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Prints the name of a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: print str(e) return print options.display(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revision [revision]") parser.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) parser.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") parser.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") parser.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") parser.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") parser.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and prints the name of the specified revision. Instead of the name, several options can be used to print locations. If more than one is specified, the last one is used. """ help_tree_spec() return def require_version_exists(version, spec): if not version.exists(): raise cmdutil.CantDetermineVersion(spec, "The version %s does not exist." \ % version) class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ (options, args) = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp try: self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None try: iter = self.get_iterator(options.type, args, options.reverse, options.modified) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) for revision in iter: log = None if isinstance(revision, arch.Patchlog): log = revision revision=revision.revision print options.display(revision) if log is None and (options.summary or options.creator or options.date or options.merges): log = revision.patchlog if options.creator: print " %s" % log.creator if options.date: print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) if options.summary: print " %s" % log.summary if options.merges: showed_title = False for revision in log.merged_patches: if not showed_title: print " Merged:" showed_title = True print " %s" % revision def get_iterator(self, type, args, reverse, modified): if len(args) > 0: spec = args[0] else: spec = None if modified is not None: iter = cmdutil.modified_iter(modified, self.tree) if reverse: return iter else: return cmdutil.iter_reverse(iter) elif type == "archive": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return version.iter_revisions(reverse) elif type == "cacherevs": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_cacherevs(version, reverse) elif type == "library": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) return version.iter_library_revisions(reverse) elif type == "logs": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ self.tree), reverse) elif type == "missing" or type == "skip-present": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") skip = (type == "skip-present") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_missing(self.tree, version, reverse, skip_present=skip) elif type == "present": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_present(self.tree, version, reverse) elif type == "new-merges" or type == "direct-merges": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) iter = cmdutil.iter_new_merges(self.tree, version, reverse) if type == "new-merges": return iter elif type == "direct-merges": return cmdutil.direct_merges(iter) elif type == "missing-from": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") revision = cmdutil.determine_revision_tree(self.tree, spec) libtree = cmdutil.find_or_make_local_revision(revision) return cmdutil.iter_missing(libtree, self.tree.tree_version, reverse) elif type == "partner-missing": return cmdutil.iter_partner_missing(self.tree, reverse) elif type == "ancestry": revision = cmdutil.determine_revision_tree(self.tree, spec) iter = cmdutil._iter_ancestry(self.tree, revision) if reverse: return iter else: return cmdutil.iter_reverse(iter) elif type == "dependencies" or type == "non-dependencies": nondeps = (type == "non-dependencies") revision = cmdutil.determine_revision_tree(self.tree, spec) anc_iter = cmdutil._iter_ancestry(self.tree, revision) iter_depends = cmdutil.iter_depends(anc_iter, nondeps) if reverse: return iter_depends else: return cmdutil.iter_reverse(iter_depends) elif type == "micro": return cmdutil.iter_micro(self.tree) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revisions [revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") select.add_option("", "--archive", action="store_const", const="archive", dest="type", default="archive", help="List all revisions in the archive") select.add_option("", "--cacherevs", action="store_const", const="cacherevs", dest="type", help="List all revisions stored in the archive as " "complete copies") select.add_option("", "--logs", action="store_const", const="logs", dest="type", help="List revisions that have a patchlog in the " "tree") select.add_option("", "--missing", action="store_const", const="missing", dest="type", help="List revisions from the specified version that" " have no patchlog in the tree") select.add_option("", "--skip-present", action="store_const", const="skip-present", dest="type", help="List revisions from the specified version that" " have no patchlogs at all in the tree") select.add_option("", "--present", action="store_const", const="present", dest="type", help="List revisions from the specified version that" " have no patchlog in the tree, but can't be merged") select.add_option("", "--missing-from", action="store_const", const="missing-from", dest="type", help="List revisions from the specified revision " "that have no patchlog for the tree version") select.add_option("", "--partner-missing", action="store_const", const="partner-missing", dest="type", help="List revisions in partner versions that are" " missing") select.add_option("", "--new-merges", action="store_const", const="new-merges", dest="type", help="List revisions that have had patchlogs added" " to the tree since the last commit") select.add_option("", "--direct-merges", action="store_const", const="direct-merges", dest="type", help="List revisions that have been directly added" " to tree since the last commit ") select.add_option("", "--library", action="store_const", const="library", dest="type", help="List revisions in the revision library") select.add_option("", "--ancestry", action="store_const", const="ancestry", dest="type", help="List revisions that are ancestors of the " "current tree version") select.add_option("", "--dependencies", action="store_const", const="dependencies", dest="type", help="List revisions that the given revision " "depends on") select.add_option("", "--non-dependencies", action="store_const", const="non-dependencies", dest="type", help="List revisions that the given revision " "does not depend on") select.add_option("--micro", action="store_const", const="micro", dest="type", help="List partner revisions aimed for this " "micro-branch") select.add_option("", "--modified", dest="modified", help="List tree ancestor revisions that modified a " "given file", metavar="FILE[:LINE]") parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", metavar="NUMBER") parser.add_option_group(select) format = cmdutil.OptionGroup(parser, "Revision format options", "These control the appearance of listed revisions") format.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) format.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") format.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") format.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") format.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") display.add_option("-r", "--reverse", action="store_true", dest="reverse", help="Sort from newest to oldest") display.add_option("-s", "--summary", action="store_true", dest="summary", help="Show patchlog summary") display.add_option("-D", "--date", action="store_true", dest="date", help="Show patchlog date") display.add_option("-c", "--creator", action="store_true", dest="creator", help="Show the id that committed the" " revision") display.add_option("-m", "--merges", action="store_true", dest="merges", help="Show the revisions that were" " merged") parser.add_option_group(display) return parser def help(self, parser=None): """Attempt to explain the revisions command :param parser: If supplied, used to determine options """ if parser==None: parser=self.get_parser() parser.print_help() print """List revisions. """ help_tree_spec() class Get(BaseCommand): """ Retrieve a revision from the archive """ def __init__(self): self.description="Retrieve a revision from the archive" self.parser=self.get_parser() def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "get" command. """ (options, args) = self.parser.parse_args(cmdargs) if len(args) < 1: return self.help() try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None arch_loc = None try: revision, arch_loc = paths.full_path_decode(args[0]) except Exception, e: revision = cmdutil.determine_revision_arch(tree, args[0], check_existence=False, allow_package=True) if len(args) > 1: directory = args[1] else: directory = str(revision.nonarch) if os.path.exists(directory): raise DirectoryExists(directory) cmdutil.ensure_archive_registered(revision.archive, arch_loc) try: cmdutil.ensure_revision_exists(revision) except cmdutil.NoSuchRevision, e: raise CommandFailedWrapper(e) link = cmdutil.prompt ("get link") for line in cmdutil.iter_get(revision, directory, link, options.no_pristine, options.no_greedy_add): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "get" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai get revision [dir]") parser.add_option("--no-pristine", action="store_true", dest="no_pristine", help="Do not make pristine copy for reference") parser.add_option("--no-greedy-add", action="store_true", dest="no_greedy_add", help="Never add to greedy libraries") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and constructs a project tree for a revision. If the optional "dir" argument is provided, the project tree will be stored in this directory. """ help_tree_spec() return class PromptCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.prompt = "Fai> " try: self.tree = arch.tree_root() except: self.tree = None self.set_title() self.set_prompt() self.fake_aba = abacmds.AbaCmds() self.identchars += '-' self.history_file = os.path.expanduser("~/.fai-history") readline.set_completer_delims(string.whitespace) if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) def write_history(self): readline.write_history_file(self.history_file) def do_quit(self, args): self.write_history() sys.exit(0) def do_exit(self, args): self.do_quit(args) def do_EOF(self, args): print self.do_quit(args) def postcmd(self, line, bar): self.set_title() self.set_prompt() def set_prompt(self): if self.tree is not None: try: version = " "+self.tree.tree_version.nonarch except: version = "" else: version = "" self.prompt = "Fai%s> " % version def set_title(self, command=None): try: version = self.tree.tree_version.nonarch except: version = "[no version]" if command is None: command = "" sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) def do_cd(self, line): if line == "": line = "~" try: os.chdir(os.path.expanduser(line)) except Exception, e: print e try: self.tree = arch.tree_root() except: self.tree = None def do_help(self, line): Help()(line) def default(self, line): args = line.split() if find_command(args[0]): try: find_command(args[0]).do_command(args[1:]) except cmdutil.BadCommandOption, e: print e except cmdutil.GetHelp, e: find_command(args[0]).help() except CommandFailed, e: print e except arch.errors.ArchiveNotRegistered, e: print e except KeyboardInterrupt, e: print "Interrupted" except arch.util.ExecProblem, e: print e.proc.error.rstrip('\n') except cmdutil.CantDetermineVersion, e: print e except cmdutil.CantDetermineRevision, e: print e except Exception, e: print "Unhandled error:\n%s" % cmdutil.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] elif self.fake_aba.is_command(args[0]): tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass cmd = self.fake_aba.is_command(args[0]) try: cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) except KeyboardInterrupt, e: print "Interrupted" elif options.tla_fallthrough and args[0] != "rm" and \ cmdutil.is_tla_command(args[0]): try: tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass args = cmdutil.expand_prefix_alias(args, tree) arch.util.exec_safe('tla', args, stderr=sys.stderr, expected=(0, 1)) except arch.util.ExecProblem, e: pass except KeyboardInterrupt, e: print "Interrupted" else: try: try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None args=line.split() os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) except KeyboardInterrupt, e: print "Interrupted" def completenames(self, text, line, begidx, endidx): completions = [] iter = iter_command_names(self.fake_aba) try: if len(line) > 0: arg = line.split()[-1] else: arg = "" iter = iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) def completedefault(self, text, line, begidx, endidx): """Perform completion for native commands. :param text: The text to complete :type text: str :param line: The entire line to complete :type line: str :param begidx: The start of the text in the line :type begidx: int :param endidx: The end of the text in the line :type endidx: int """ try: (cmd, args, foo) = self.parseline(line) command_obj=find_command(cmd) if command_obj is not None: return command_obj.complete(args.split(), text) elif not self.fake_aba.is_command(cmd) and \ cmdutil.is_tla_command(cmd): iter = cmdutil.iter_supported_switches(cmd) if len(args) > 0: arg = args.split()[-1] else: arg = "" if arg.startswith("-"): return list(iter_munged_completions(iter, arg, text)) else: return list(iter_munged_completions( iter_file_completions(arg), arg, text)) elif cmd == "cd": if len(args) > 0: arg = args.split()[-1] else: arg = "" iter = iter_dir_completions(arg) iter = iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] return list(iter_munged_completions(iter_file_completions(arg), arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: print e def iter_command_names(fake_aba): for entry in cmdutil.iter_combine([commands.iterkeys(), fake_aba.get_commands(), cmdutil.iter_tla_commands(False)]): if not suggestions.has_key(str(entry)): yield entry def iter_file_completions(arg, only_dirs = False): """Generate an iterator that iterates through filename completions. :param arg: The filename fragment to match :type arg: str :param only_dirs: If true, match only directories :type only_dirs: bool """ cwd = os.getcwd() if cwd != "/": extras = [".", ".."] else: extras = [] (dir, file) = os.path.split(arg) if dir != "": listingdir = os.path.expanduser(dir) else: listingdir = cwd for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): if dir != "": userfile = dir+'/'+file else: userfile = file if userfile.startswith(arg): if os.path.isdir(listingdir+'/'+file): userfile+='/' yield userfile elif not only_dirs: yield userfile def iter_munged_completions(iter, arg, text): for completion in iter: completion = str(completion) if completion.startswith(arg): yield completion[len(arg)-len(text):] def iter_source_file_completions(tree, arg): treepath = cmdutil.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in tree.iter_inventory(dirs, source=True, both=True): file = file_completion_match(file, treepath, arg) if file is not None: yield file def iter_untagged(tree, dirs): for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, categories=arch_core.non_root, control_files=True): yield file.name def iter_untagged_completions(tree, arg): """Generate an iterator for all visible untagged files that match arg. :param tree: The tree to look for untagged files in :type tree: `arch.WorkingTree` :param arg: The argument to match :type arg: str :return: An iterator of all matching untagged files :rtype: iterator of str """ treepath = cmdutil.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in iter_untagged(tree, dirs): file = file_completion_match(file, treepath, arg) if file is not None: yield file def file_completion_match(file, treepath, arg): """Determines whether a file within an arch tree matches the argument. :param file: The rooted filename :type file: str :param treepath: The path to the cwd within the tree :type treepath: str :param arg: The prefix to match :return: The completion name, or None if not a match :rtype: str """ if not file.startswith(treepath): return None if treepath != "": file = file[len(treepath)+1:] if not file.startswith(arg): return None if os.path.isdir(file): file += '/' return file def iter_modified_file_completions(tree, arg): """Returns a list of modified files that match the specified prefix. :param tree: The current tree :type tree: `arch.WorkingTree` :param arg: The prefix to match :type arg: str """ treepath = cmdutil.tree_cwd(tree) tmpdir = cmdutil.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) for line in arch.iter_delta(revision, tree, changeset): if isinstance(line, arch.FileModification): file = file_completion_match(line.name[1:], treepath, arg) if file is not None: completions.append(file) shutil.rmtree(tmpdir) return completions def iter_dir_completions(arg): """Generate an iterator that iterates through directory name completions. :param arg: The directory name fragment to match :type arg: str """ return iter_file_completions(arg, True) class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" def do_command(self, cmdargs): if len(cmdargs)!=0: raise cmdutil.GetHelp prompt = PromptCmd() try: prompt.cmdloop() finally: prompt.write_history() class AddID(BaseCommand): """ Adds an inventory id for the given file """ def __init__(self): self.description="Add an inventory id for a given file" def get_completer(self, arg, index): tree = arch.tree_root() return iter_untagged_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp #if options.id and len(args) != 1: # print "If --id is specified, only one file can be named." # return method = tree.tagging_method if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): if method == "explicit": options.id_type == explicit else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): print "add-id not supported for \"%s\" tagging method" % \ method return if options.id_type == "auto": if method != "tagline" and method != "explicit": print "add-id not supported for \"%s\" tagging method" % method return else: options.id_type = method if options.untagged: args = None self.add_ids(tree, options.id_type, args) def add_ids(self, tree, id_type, files=()): """Add inventory ids to files. :param tree: the tree the files are in :type tree: `arch.WorkingTree` :param id_type: the type of id to add: "explicit" or "tagline" :type id_type: str :param files: The list of files to add. If None do all untagged. :type files: tuple of str """ untagged = (files is None) if untagged: files = list(iter_untagged(tree, None)) previous_files = [] while len(files) > 0: previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) elif id_type == "tagline": for file in files: try: cmdutil.add_tagline_or_explicit_id(file) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: pass #do inventory after tagging until no untagged files are encountered if untagged: files = [] for file in iter_untagged(tree, None): if not file in previous_files: files.append(file) else: break def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") # ddaa suggests removing this to promote GUIDs. Let's see who squalks. # parser.add_option("-i", "--id", dest="id", # help="Specify id for a single file", default=None) parser.add_option("--tltl", action="store_true", dest="lord_style", help="Use Tom Lord's style of id.") parser.add_option("--explicit", action="store_const", const="explicit", dest="id_type", help="Use an explicit id", default="auto") parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Adds an inventory to the specified file(s) and directories. If --untagged is specified, adds inventory to all untagged files and directories. """ return class Merge(BaseCommand): """ Merges changes from other versions into the current tree """ def __init__(self): self.description="Merges changes from other versions" try: self.tree = arch.tree_root() except: self.tree = None def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError completions = list(ancillary.iter_partners(self.tree, self.tree.tree_version)) if len(completions) == 0: completions = list(self.tree.iter_log_versions()) aliases = [] try: for completion in completions: alias = ancillary.compact_alias(str(completion), self.tree) if alias: aliases.extend(alias) for completion in completions: if completion.archive == self.tree.tree_version.archive: aliases.append(completion.nonarch) except Exception, e: print e completions.extend(aliases) return completions def do_command(self, cmdargs): """ Master function that perfoms the "merge" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if options.diff3: action="star-merge" else: action = options.action if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) if cmdutil.has_changed(self.tree.tree_version): raise UncommittedChanges(self.tree) if len(args) > 0: revisions = [] for arg in args: revisions.append(cmdutil.determine_revision_arch(self.tree, arg)) source = "from commandline" else: revisions = ancillary.iter_partner_revisions(self.tree, self.tree.tree_version) source = "from partner version" revisions = misc.rewind_iterator(revisions) try: revisions.next() revisions.rewind() except StopIteration, e: revision = cmdutil.tag_cur(self.tree) if revision is None: raise CantDetermineRevision("", "No version specified, no " "partner-versions, and no tag" " source") revisions = [revision] source = "from tag source" for revision in revisions: cmdutil.ensure_archive_registered(revision.archive) cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % (revision, source))) if action=="native-merge" or action=="update": if self.native_merge(revision, action) == 0: continue elif action=="star-merge": try: self.star_merge(revision, options.diff3) except errors.MergeProblem, e: break if cmdutil.has_changed(self.tree.tree_version): break def star_merge(self, revision, diff3): """Perform a star-merge on the current tree. :param revision: The revision to use for the merge :type revision: `arch.Revision` :param diff3: If true, do a diff3 merge :type diff3: bool """ try: for line in self.tree.iter_star_merge(revision, diff3=diff3): cmdutil.colorize(line) except arch.util.ExecProblem, e: if e.proc.status is not None and e.proc.status == 1: if e.proc.error: print e.proc.error raise MergeProblem else: raise def native_merge(self, other_revision, action): """Perform a native-merge on the current tree. :param other_revision: The revision to use for the merge :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ other_tree = cmdutil.find_or_make_local_revision(other_revision) try: if action == "native-merge": ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, other_revision) elif action == "update": ancestor = cmdutil.tree_latest(self.tree, other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) if (ancestor == other_revision): cmdutil.colorize(arch.Chatter("* Skipping redundant merge" % ancestor)) return 0 delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line) return 1 def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai merge [VERSION]") parser.add_option("-s", "--star-merge", action="store_const", dest="action", help="Use star-merge", const="star-merge", default="native-merge") parser.add_option("--update", action="store_const", dest="action", help="Use update picker", const="update") parser.add_option("--diff3", action="store_true", dest="diff3", help="Use diff3 for merge (implies star-merge)") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Performs a merge operation using the specified version. """ return class ELog(BaseCommand): """ Produces a raw patchlog and invokes the user's editor """ def __init__(self): self.description="Edit a patchlog to commit" try: self.tree = arch.tree_root() except: self.tree = None def do_command(self, cmdargs): """ Master function that perfoms the "elog" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if self.tree is None: raise arch.errors.TreeRootError edit_log(self.tree) def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai elog") return parser def help(self, parser=None): """ Invokes $EDITOR to produce a log for committing. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Invokes $EDITOR to produce a log for committing. """ return def edit_log(tree): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge :param tree: The tree to edit the log for :type tree: `arch.WorkingTree` """ #ensure we have an editor before preparing the log cmdutil.find_editor() log = tree.log_message(create=False) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) log = tree.log_message(create=True) log_is_new = True tmplog = log.name template = tree+"/{arch}/=log-template" if not os.path.exists(template): template = os.path.expanduser("~/.arch-params/=log-template") if not os.path.exists(template): template = None if template: shutil.copyfile(template, tmplog) new_merges = list(cmdutil.iter_new_merges(tree, tree.tree_version)) log["Summary"] = merge_summary(new_merges, tree.tree_version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): mergestuff = cmdutil.log_for_merge(tree) log.description += mergestuff log.save() try: cmdutil.invoke_editor(log.name) except: if log_is_new: os.remove(log.name) raise def merge_summary(new_merges, tree_version): if len(new_merges) == 0: return "" if len(new_merges) == 1: summary = new_merges[0].summary else: summary = "Merge" credits = [] for merge in new_merges: if arch.my_id() != merge.creator: name = re.sub("<.*>", "", merge.creator).rstrip(" "); if not name in credits: credits.append(name) else: version = merge.revision.version if version.archive == tree_version.archive: if not version.nonarch in credits: credits.append(version.nonarch) elif not str(version) in credits: credits.append(str(version)) return ("%s (%s)") % (summary, ", ".join(credits)) class MirrorArchive(BaseCommand): """ Updates a mirror from an archive """ def __init__(self): self.description="Update a mirror from an archive" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if len(args) > 1: raise GetHelp try: tree = arch.tree_root() except: tree = None if len(args) == 0: if tree is not None: name = tree.tree_version() else: name = cmdutil.expand_alias(args[0], tree) name = arch.NameParser(name) to_arch = name.get_archive() from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) limit = name.get_nonarch() iter = arch_core.mirror_archive(from_arch,to_arch, limit) for line in arch.chatter_classifier(iter): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Updates a mirror from an archive. If a branch, package, or version is supplied, only changes under it are mirrored. """ return def help_tree_spec(): print """Specifying revisions (default: tree) Revisions may be specified by alias, revision, version or patchlevel. Revisions or versions may be fully qualified. Unqualified revisions, versions, or patchlevels use the archive of the current project tree. Versions will use the latest patchlevel in the tree. Patchlevels will use the current tree- version. Use "alias" to list available (user and automatic) aliases.""" def help_aliases(tree): print """Auto-generated aliases acur : The latest revision in the archive of the tree-version. You can specfy a different version like so: acur:foo--bar--0 (aliases can be used) tcur : (tree current) The latest revision in the tree of the tree-version. You can specify a different version like so: tcur:foo--bar--0 (aliases can be used). tprev : (tree previous) The previous revision in the tree of the tree-version. To specify an older revision, use a number, e.g. "tprev:4" tanc : (tree ancestor) The ancestor revision of the tree To specify an older revision, use a number, e.g. "tanc:4" tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") tmod : (tree modified) The latest revision to modify a given file (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") ttag : (tree tag) The revision that was tagged into the current tree revision, according to the tree. tagcur: (tag current) The latest revision of the version that the current tree was tagged from. mergeanc : The common ancestor of the current tree and the specified revision. Defaults to the first partner-version's latest revision or to tagcur. """ print "User aliases" for parts in ancillary.iter_all_alias(tree): print parts[0].rjust(10)+" : "+parts[1] class Inventory(BaseCommand): """List the status of files in the tree""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() categories = [] if (options.source): categories.append(arch_core.SourceFile) if (options.precious): categories.append(arch_core.PreciousFile) if (options.backup): categories.append(arch_core.BackupFile) if (options.junk): categories.append(arch_core.JunkFile) if len(categories) == 1: show_leading = False else: show_leading = True if len(categories) == 0: categories = None if options.untagged: categories = arch_core.non_root show_leading = False tagged = False else: tagged = None for file in arch_core.iter_inventory_filter(tree, None, control_files=options.control_files, categories = categories, tagged=tagged): print arch_core.file_line(file, category = show_leading, untagged = show_leading, id = options.ids) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai inventory [options]") parser.add_option("--ids", action="store_true", dest="ids", help="Show file ids") parser.add_option("--control", action="store_true", dest="control_files", help="include control files") parser.add_option("--source", action="store_true", dest="source", help="List source files") parser.add_option("--backup", action="store_true", dest="backup", help="List backup files") parser.add_option("--precious", action="store_true", dest="precious", help="List precious files") parser.add_option("--junk", action="store_true", dest="junk", help="List junk files") parser.add_option("--unrecognized", action="store_true", dest="unrecognized", help="List unrecognized files") parser.add_option("--untagged", action="store_true", dest="untagged", help="List only untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists the status of files in the archive: S source P precious B backup J junk U unrecognized T tree root ? untagged-source Leading letter are not displayed if only one kind of file is shown """ return class Alias(BaseCommand): """List or adjust aliases""" def __init__(self): self.description=self.__doc__ def get_completer(self, arg, index): if index > 2: return () try: self.tree = arch.tree_root() except: self.tree = None if index == 0: return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] elif index == 1: return cmdutil.iter_revision_completions(arg, self.tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree = arch.tree_root() except: self.tree = None try: options.action(args, options) except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments :param args: The list of commandline arguments :type args: list of str :param options: The commandline options """ if len(args) == 0: help_aliases(self.tree) return elif len(args) == 1: self.print_alias(args[0]) elif (len(args)) == 2: self.add(args[0], args[1], options) else: raise cmdutil.GetHelp def print_alias(self, alias): answer = None for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] if answer is not None: print answer else: print "The alias %s is not assigned." % alias def add(self, alias, expansion, options): """Add or modify aliases :param alias: The alias name to create/modify :type alias: str :param expansion: The expansion to assign to the alias name :type expansion: str :param options: The commandline options """ newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, self.tree)) ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) elif not written: newlist+=new_line written = True if not written: newlist+=new_line self.write_aliases(newlist, options) def delete(self, args, options): """Delete the specified alias :param args: The list of arguments :type args: list of str :param options: The commandline options """ deleted = False if len(args) != 1: raise cmdutil.GetHelp newlist = "" for pair in self.get_iterator(options): if pair[0] != args[0]: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: raise errors.NoSuchAlias(args[0]) self.write_aliases(newlist, options) def get_alias_file(self, options): """Return the name of the alias file to use :param options: The commandline options """ if options.tree: if self.tree is None: self.tree == arch.tree_root() return str(self.tree)+"/{arch}/+aliases" else: return "~/.aba/aliases" def get_iterator(self, options): """Return the alias iterator to use :param options: The commandline options """ return ancillary.iter_alias(self.get_alias_file(options)) def write_aliases(self, newlist, options): """Safely rewrite the alias file :param newlist: The new list of aliases :type newlist: str :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) file = cmdutil.NewFileVersion(filename) file.write(newlist) file.commit() def get_parser(self): """ Returns the options parser to use for the "alias" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") parser.add_option("-d", "--delete", action="store_const", dest="action", const=self.delete, default=self.arg_dispatch, help="Delete an alias") parser.add_option("--tree", action="store_true", dest="tree", help="Create a per-tree alias", default=False) return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists current aliases or modifies the list of aliases. If no arguments are supplied, aliases will be listed. If two arguments are supplied, the specified alias will be created or modified. If -d or --delete is supplied, the specified alias will be deleted. You can create aliases that refer to any fully-qualified part of the Arch namespace, e.g. archive, archive/category, archive/category--branch, archive/category--branch--version (my favourite) archive/category--branch--version--patchlevel Aliases can be used automatically by native commands. To use them with external or tla commands, prefix them with ^ (you can do this with native commands, too). """ class RequestMerge(BaseCommand): """Submit a merge request to Bug Goo""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """Submit a merge request :param cmdargs: The commandline arguments :type cmdargs: list of str """ cmdutil.find_editor() parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree=arch.tree_root() except: self.tree=None base, revisions = self.revision_specs(args) message = self.make_headers(base, revisions) message += self.make_summary(revisions) path = self.edit_message(message) message = self.tidy_message(path) if cmdutil.prompt("Send merge"): self.send_message(message) print "Merge request sent" def make_headers(self, base, revisions): """Produce email and Bug Goo header strings :param base: The base revision to apply merges to :type base: `arch.Revision` :param revisions: The revisions to replay into the base :type revisions: list of `arch.Patchlog` :return: The headers :rtype: str """ headers = "To: gnu-arch-users@gnu.org\n" headers += "From: %s\n" % options.fromaddr if len(revisions) == 1: headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary else: headers += "Subject: [MERGE REQUEST]\n" headers += "\n" headers += "Base-Revision: %s\n" % base for revision in revisions: headers += "Revision: %s\n" % revision.revision headers += "Bug: \n\n" return headers def make_summary(self, logs): """Generate a summary of merges :param logs: the patchlogs that were directly added by the merges :type logs: list of `arch.Patchlog` :return: the summary :rtype: str """ summary = "" for log in logs: summary+=str(log.revision)+"\n" summary+=log.summary+"\n" if log.description.strip(): summary+=log.description.strip('\n')+"\n\n" return summary def revision_specs(self, args): """Determine the base and merge revisions from tree and arguments. :param args: The parsed arguments :type args: list of str :return: The base revision and merge revisions :rtype: `arch.Revision`, list of `arch.Patchlog` """ if len(args) > 0: target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: target_revision = cmdutil.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] else: if self.tree is None: raise CantDetermineRevision("", "Not in a project tree") merge_iter = cmdutil.iter_new_merges(self.tree, target_revision.version, False) merges = [f for f in cmdutil.direct_merges(merge_iter)] return (target_revision, merges) def edit_message(self, message): """Edit an email message in the user's standard editor :param message: The message to edit :type message: str :return: the path of the edited message :rtype: str """ if self.tree is None: path = os.get_cwd() else: path = self.tree path += "/,merge-request" file = open(path, 'w') file.write(message) file.flush() cmdutil.invoke_editor(path) return path def tidy_message(self, path): """Validate and clean up message. :param path: The path to the message to clean up :type path: str :return: The parsed message :rtype: `email.Message` """ mail = email.message_from_file(open(path)) if mail["Subject"].strip() == "[MERGE REQUEST]": raise BlandSubject request = email.message_from_string(mail.get_payload()) if request.has_key("Bug"): if request["Bug"].strip()=="": del request["Bug"] mail.set_payload(request.as_string()) return mail def send_message(self, message): """Send a message, using its headers to address it. :param message: The message to send :type message: `email.Message`""" server = smtplib.SMTP() server.sendmail(message['From'], message['To'], message.as_string()) server.quit() def help(self, parser=None): """Print a usage message :param parser: The options parser to use :type parser: `cmdutil.CmdOptionParser` """ if parser is None: parser = self.get_parser() parser.print_help() print """ Sends a merge request formatted for Bug Goo. Intended use: get the tree you'd like to merge into. Apply the merges you want. Invoke request-merge. The merge request will open in your $EDITOR. When no TARGET is specified, it uses the current tree revision. When no MERGE is specified, it uses the direct merges (as in "revisions --direct-merges"). But you can specify just the TARGET, or all the MERGE revisions. """ def get_parser(self): """Produce a commandline parser for this command. :rtype: `cmdutil.CmdOptionParser` """ parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") return parser commands = { 'changes' : Changes, 'help' : Help, 'update': Update, 'apply-changes':ApplyChanges, 'cat-log': CatLog, 'commit': Commit, 'revision': Revision, 'revisions': Revisions, 'get': Get, 'revert': Revert, 'shell': Shell, 'add-id': AddID, 'merge': Merge, 'elog': ELog, 'mirror-archive': MirrorArchive, 'ninventory': Inventory, 'alias' : Alias, 'request-merge': RequestMerge, } suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", 'diff-rev' : "To compare two revisions, use \"changes\".", 'undo' : "To undo local changes, use \"revert\".", 'undelete' : "To undo only deletions, use \"revert --deletions\"", 'missing-from' : "Try \"revisions --missing-from\".", 'missing' : "Try \"revisions --missing\".", 'missing-merge' : "Try \"revisions --partner-missing\".", 'new-merges' : "Try \"revisions --new-merges\".", 'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", 'logs' : "Try \"revisions --logs\"", 'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", 'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", 'change-version' : "Try \"update REVISION\"", 'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", 'rev-depends' : "Use revisions --dependencies", 'auto-get' : "Plain get will do archive lookups", 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", 'file-revert' : "Use revert FILE" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.2.1/test_patches_data/orig-2000064400000000000000000000441021046102023000161350ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/orig-3000064400000000000000000000441611046102023000161430ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/orig-4000064400000000000000000000441021046102023000161370ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/orig-5000064400000000000000000000441021046102023000161400ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/orig-6000064400000000000000000000441021046102023000161410ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.2.1/test_patches_data/orig-7000064400000000000000000000000261046102023000161370ustar 00000000000000No terminating newlinepatchkit-0.2.1/test_patches_data/patchtext.patch000064400000000000000000000017341046102023000201440ustar 00000000000000--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: