tortoisehg-2.10/0000755000076400007640000000000012235634575012705 5ustar stevestevetortoisehg-2.10/icons/0000755000076400007640000000000012235634575014020 5ustar stevestevetortoisehg-2.10/icons/menumerge.ico0000664000076400007640000001373612100577421016477 0ustar stevesteve  6 h  F( @ NNNOON NO T VONNO Tfg XONNO Tfggg ZONNO Tfggggg [ON&NO Tfggggggg]ON0NO Teffggggfff^ON<NNNNNNNfggfNNNNNNNKNNNNNNNghhgNNNNNNNNNjkkjNNNNmnnmNNNNoppoNNNNrssrNNNN u!v!v uNNNO$y$y$y$yONN8 T'|'|'|'| RN4P`)~)~)~)~]NvNP%w,؁,؁,؁,؁!rQNP\/ۄ/ۄ/ۄ/ۄ/ۄ/ڃXON[ S.ك1އ1އ1އ1އ1އ1އ,ׁQNFN,P+~4442އ3߈444'yPN$N.P#t7777ca7777 pPN9NIP$t::::#rQ Qg::::'xPNSOlQ-<<<>>>/ցQNKNQ&u>>>>5߉ UQNN QZ9>>>>*{QNCN!Q)z>>>>9ZQN N Q`<>>>$sQN/N+Q$t>>><` QNN Q`<>lQN N Qm><` QNN Q`f RNN Rf` QNN Q RN N R QNNNNN???(  ???_?_???_?_?_?_???_?__?_?__??_??_?_??_?_?_?_?_??_?_?_?__?_?_?_?__?_?_?_?_?_?_?_?_?_?_?_?_?_??_?_??_?_??_?_?_?_??O?Cc(  p@u?ϵu?ϭp@pBr?b>c?r?pBu?Ϯ{KjDkEzJu?jAwD_cAcAwD_jAdAe@߯vF?dAa?}L߮|N/dBeEkKQ|O/vH?e@ߤmL߱rjJP߮~Q?vJ/q@ߠ`>x~W}ϡcArJuJ/q?kK~uI?q@ߣkKsZmF`>~ϭ[ppp00tortoisehg-2.10/icons/menurevert.ico0000664000076400007640000001373612100577421016707 0ustar stevesteve  6 h  F( @ J J J J J J J J J J J J J JˇK3J јjјjјjјjјjΔfɎ`ĉYP_1L"J gJ јjјjјjјjјj͓dȍ^QQ'M!KJ јjјjјjјjЗȋc~PN#L"hJ јjјjјjјjϖh}PM"J HJ јjјjјjјjΕfRM#耀J јjјjјjјj͓eɎ_U*J"LGJ јjјjјjјj̒dȍ^qDN$K!K J јjЗiјjЗiˑbNj\UN#M (I1K K J јjh<ϖhϕgʐaƊ[Ud8M#K J JZJ ŋ^J oCΔfɎ`ʼnZT{KO$K iJ"LK"K!U J k@K"M#Xȍ^ĈXR|LqAL"I ?J!K!S'J xJ M#H J#`V+Ƌ\ÆWQ{KuEf7L"Kb33J ].M!J O%N%çl?…VPzItCo=c2Q%K!J"LHCN#d3K J JSLM#m?~NyHsBm*\J mwJ <KKF^_[^_[^_[~J <KKF^_[^_[^_[8rP7f7fMM:J <J m7zT44@aEI!>KKF^_[^_[^_[6e45q:?3<--KKF^_[^_[^_[y|x5k44q6>3333J m:N>5~U5sOxnbaWG4{45b670JJ mLmX44JP=}l\5j44z670yJ muzs4ք45y6zS6[4چ44k670TJ sTʸʸʸ9vR444444Ճ6S>55,D"J TJ TJ TJ TJ TO=*p6U?5_4k4l5Z6G866/#33364-.75/L56/O55,%( @ 44/664.6:169056074.T... P P P P P P P P P P P P P P 98.64.6T?5]5f6jJ691691M;'J J J J J J J J J J J J J J tE$76.4t44444؅8P=I;,J J {s:ZE4444445c;;0J J ź:?64Ԃ444446mLB;.J J źX[T;L=5\4y4Ճ5h9hK87/hB%J J źRUN64.64.@B:z{wV8J xJ J ź_;J x87164.64.64.64.64.64.64.HGB_;J x64.64.|T7J x64.64.8<3:TA:TA:TA:UB:UB8=4aH4J x87164.64.64.64.64.64.64.HGB;XC44444:eJaH4J xJ J ź9@64ވ4444:hKYE3J xJ J ź;<55_44445g980H"87164.64.64.64.64.64.64.FFA76/5i4444݇8E6B;.... 64.64.8715o4444}7G8591<--64.64.y{w7924t4444Ѐ5L:690333 87164.64.64.64.64.64.64.ED?mpk9>54x4444΀6E7691333J J ź86064.64.64.64.64.WYSrto:B74Ё4444}69174/bJ J źDF?5Z4|4|4|5e870_=a>a>fCC?35W44445wQ691J J źUYR5W4444؅75/zZ@nnnfQ@8Q=44445g681J J źpqm8iK44447qO>?5B=05Y44445l66/J J ź64.J J ź~:J<4ۆ4444444444445f64.55,:J J tUfffffff}c?=46uP4م44444444445n65.75.tD"J J J J J J J J J J jB$:9/8?45b4ވ4444444v5iJ64.66/999 64.u68/57/6I95_E5wQ5|T5`E6D764.69155,:33355.M74-k65/75/58064.55-e66+/?????????tortoisehg-2.10/icons/menuabout.ico0000664000076400007640000000344612100577421016507 0ustar stevesteve h&  (  @2<|2<|2<|>]2<]2]999>]22]|]999>>]]?2]?2i]?>RRR>*]?||___]?]?>>>999llٓli]?>]<Pxxxٓllllll]?ll<RRRڐڐ999PEEElllll___999<P999EEEP0i(  pH*>%?7?!+- z y0?A@2?ta_%/ SM=!?A4?+n _f:**Az>$?V70frMQN\Q' rE*/a*A-"k^('a^lI+ [)yA"?wooeܷknU(L< VxjSEމΈc^XߖpDDDbSE}}`iZgb]mQ1?Og_D3U.]7pW6?oV5?}b>/w ]/G#?{R':w{ qj,vp4Bwyt4BvzC4~  vz:ppp0tortoisehg-2.10/icons/menucheckout.ico0000664000076400007640000001373612100577421017205 0ustar stevesteve  6 h  F( @ NNNNNNNNNNNNNNNNgggggggggf UON Ngggghijki SONNgghjlmnn UPNgilnprs YPNNilort!vaPNNlor!u#x%zmQN<lƓȽ ȽƓl<Nnr!u$x&{)~+׀ XN\, 6gz^J@5+ ,Np t#x&{)~,ف.ۄ'yQN,WcOOOOMB#Nr]$x(},؁/܄2߇4aQPWdOOOOMB7NmNb*.ڃ1އ586 TNRPVeOOOOMB7N^OP#v.ۄ2߈6:=/؂PN"PUeOOOOMB7NPN#O^ W.ك2އ58:8 qQNPUfOOOOMB7NPQk0܅2߈5542߇_QPTgOOOOMB7NNTN#P#t/܄0݆1ކ0݆.ۄ+׀\nNSgOOOOMB7NQNN<P#u,ف-ق,؁+׀)~&zadBhOOOOMB7NKP r(}(}'|%z#x ug W-`OOOOMB7NQPb#w#x!vtroj^[NN0OOOOMB7NPR_inkh_] TBviOOOOMB7N5OPOU ]M˱jOOOOMC7PPkOOOOMC7PPkOOOONC7PPlOOOONC7PPlOOOONC8PARXF'1686J]pp]J4DDDD$ 3I]pp]I3 $0_Ƈ ɵ  ɵƇ_0(  22222<<O??=:7?<<<T4aO(y2<$m<=4aO(y'f2<$m<=Boa<1$m$m$mAp^<1$m$m$mZ<12<<2<W<2}X<2}=;pY<2}=;pZ<3|=5eM%y3|;-DZ<&m=,MZ<=O254/+O(    / 477'L5233795 :< P:/QD&- IRW15fV,#r1MY>,]Q*-75\"gI=N*-<'-(+-cL*.Wf-zK}M*.73± ?>KF5;@Ǩtortoisehg-2.10/icons/menudiff.ico0000664000076400007640000000344612100577421016305 0ustar stevesteve h&  (  @Yk<Y:4c#ibk<\8@<4m2r,n%eZ k<[5EB@=4u?E{;r,ibeBEB@=4cE{;u1r,Ebo:BEBBZ9c@Lu1r,KPBF]7ly{;eUEe8[5tVyR@n%Z `wU* [5cEu?j=Er,bM`MMa5gSLEu?e8{;BMUMKa5|JSML@m2^*UVVUUMB|JSMEEtVku7s-j `f(oNCB?9GQBw5p)r-~@[g;CB@H~LuR~ArQ{:WnHg;DPY&uyA|KuNiQQ{ZpN](u[!yAwGvvQvXp)[ a'F-<^(u[!MvW~Dr>v@JdA;NV[$uYQJ}Cm8be LRRMR~INNG{>lB` WURKP~G |G}NMVr@uk4ua!UMV|^! xA yDWuCum4l4j3uZZd( tortoisehg-2.10/icons/menuclone.ico0000664000076400007640000001373612100577421016500 0ustar stevesteve  6 h  F( @ 0_ƈɷ ɹƉ`2$ 2e|`LA5* (*VdOOOOMA#PVeOOOOMB7PVeOOOOMB7PUfOOOOMB7PTgOOOOMB7PTgOOOOMB73fōȺ PShOOOOMB7& 8maNDPShOOOOMB7.XcOOPRiOOOOMB7PWdOOPQjOOOOMB7PVeOOPQjOOOOMB7PVeOOPPkOOOOMC7PUfOOPPkOOOOMC7PUfOOPPlOOOONC7PTgOOPPmOOOONC7PShOOPPmOOOONC8PShOOPCQZI# '178PRiOO5J^qr^J3PRiOOKO PQjOONSPQkOOD7MbvvbN6 )PPkOOOO>(  ȻƏg4PPlOOOOMB7PPlOOOOMC7PARXE'067:NattaN8IIII% 7NattaN7 %0_ƈ ɷ  ɷƈ_0??????(  7<<<<<!]]]]]#!@}}}}}+!@}}C)7;<<@A)!]]]]@A)5}}}}GA(?}}PA';OA#;OuuAS9KddddYDl6}LLLLLL6}7#6}}uu}GSk6ЪddddOOlLڪLLLLLڪ(  <;;+0MMT |?}}C)<;;;A)0MMT;A)?}}6A';6A#;6pppAl6'LLLL6A#k6pppAlLLLLp0000000ptortoisehg-2.10/icons/hg.ico0000644000076400007640000013257112121124312015074 0ustar stevesteve (~  c0@@ (B600 %)x  ѝ hy  ( @-.-:<;KML]_^lom}}eE2cG_Fvg_{T+_ܥ yUpޑ _!g`L,\OGlxbJjxOeR9?@%O7P(˿G/;?oɈɘ_˪̼??( (+)697KMLZZZkml_44og?wl?_v_?g}wkC/Vgvk$XwvwwgHwgwvvwwg_wwvxOwwguvgw( GJHUXV^[]^a_gihutu~/Xor//thx򈈈p000PNG  IHDR>a IDATxw|߳%}nHD%"QPA.xU+Wr)UQbA  !C;!nzBHlˎd 5dw̜9<9O IRkס!^@d{FW+4E膽G7T@z}U޿$(.PlNuz>`epaX{ 7:Պlf9V+$T*J%H`5HןWl\E1.Vl6c20LTUUa2X,lAt:A%灅|^_xnB\E'0\ee%%%%aZN wwz>Y+u \sEQ< ]+W h4( WEO9zlΎk(\KW(h4L `^o&pA{\_g[ch AR:Mןi\E1j&*ʛjX,SZZUl@`` sz~yj&(VJ2J]kNT*BBBh4NO5gZ$I_ Pg}9 ZXJPW%֯_ZE_D$rrr!!pss#""•X !3g/++P(JKKe3@&L@ff(TϹ*0{^$[JWJJJ:t`ԩMExF ( X),,Ohh(L:ϳpB4X^J#Ν;j|LQQ@SXXo̘1(WrV%K/z7>\QQ_F(,,F̙39vֈxQ;U `X>SIʞ+Evv6.]0a֯_(QjQ,V#Nm߾Wʰl-]Vlxܹs |p9l5teBll痔tuTVVRPPQ*F (>z9ѕ$Iľ~k*4 O>$gŊsEQti%UZ:.{\.5yyy9v_p?ܧU1NӹsI BCB]u]t jgyRɇ~dLo=Z:.,,,̧WEqq1fHc/R׷QKx\ ;wGyAor\V![' ~`Zff>#;DQصk5W?_&̌;۷siՋ bKQ?$Ν;ӻwo$IbʕSuAj$~_E@s*;ѺD9~]L8PzIll,VUV<%bkZɓl߾N塲R;97wW״OСC>:]u $rJd2?L/ ACX, ٳg{}5ppNh4ƍBQ˷̌Lz쉗}P*:.r٘[{B$v'h0WXXSVmhhO( {3g`ƍeBy9##t9weee?sQXXӧ1S#k!!!PΜ9Æ_60dڷo/HJJbÆ gwwwZu]ȑ#mEQ ZHMMP(Tf@NNN,u9{l<%.[ j* ߝDFF^65#$cC-~T*CΟ?OFFe9h6 EdOsm6:v풿"**Lp G6m3!!!}*++b\Br޽/f`0>|Q8q"ʑYfo>;+.piG: 66aSxbZٸq^`VPRR”)SNL&OκuJ%:Np${5-#Ɵxl۶oIIIE׶ZTTTеkW6lpڵk޷4%s 6IpCkgwUTb6ro>𯌌 >C^uy&l6III7sѴh۶-:__Av휶@ }Gݎ!>>?j6mo5 F,Ǐ3?s:u*Æ sYbvZ}Qf͚Err^^^Ӿ}{"""KBCCwul;P#^ H=.Y`777oQ*]ZLgϞ}ٯ1a:wg}FEEZ\yRTT* 8xZ-F$Ir(WpI۩P;b 0HUYY)UWW7HxifFFjJyG8rH%/ J:n{ee ,;Xd /&%%QFѾ}{̔X,ڵKV O||'N`ꔩ8qѣG3~xٻw/w}wBIHKKcʕL:QFo IIRнf cD7 u:@Bړž|Q᮪aXE"##yZjLͻ`4<'|BV=ӫZ&22NwGz:^l VO>MZZ*&XUUUb$),,Dղn:oK/Ċ+\}oy #JYYY+ٯΗ_~[oEaa!GM6/O?QXXHrr2'Ob q>#(ˋh]zW:jN>FjcbbbfΚS^^^ǫT*Z-~~~RPP@ii)^^^DDDw߱h"Cnϑ$I'I_\uXXbРA( ֮]͛1r-?{ǩ79rKrℳSO=]w%wܹ.y>}dӯ4~wS߶m:ڥD$ V_~Lz~\ac^^^NyyKe~~~xyyxbV\I=x'͛_bZRRriuS? ԻwoOn8pk֬3_~Yb]?ӹsg'wvFFFF&{c֔e;||| d|uIHHW^)aV~C9yu2h ƭʮ]CZFo?CP^^Nxx8= 8GFP /VHd3 KnN4ٜ~X-V*5rss̰Ik Fd06=v"l9g,^]wEuu5jeCߩ+**ɑNnڴ5֔]ԴUm۶%$/I!Kth4^ FuZ^O~i'xyt\g[h5 all猌  ^^^8ŁhmO>] ^5 괺xG7`ƌx{{`!845q)|||d*]r׍k`4봺ϙNƨdܹӞMaa!%%%rϮgFC6mjoJ] F|X0۷W^.˶ZCgGtT*^^^:u~ NJJLB^:.ѰxرcleCjNڵj%33S\~_~El*Hy `y.FsۛΝ;;}l\@VHQQ<$ FܱqqqNBfСCj<<<8t萓uCeNA 11 `0v[W\mBhh(QQQNߝ:u2YWOn:p1^V[,=6 @(fLRI *_|mN|=tM AsoZ9t Ȼ)))u ۷_|Q;0DQTIt a$In.$h5 IR'IjE1Iw%}{$Io]tctI***CTߺt{ꩧHNNFTrM7PVV|M))) A~$^;tWB$I `$I(ؼykO)m6[g9 aZ9|0L&Gn'( &Lu눏!CBB}fnbZV+eeeS]]j*Apss??;_ /(~L>[ =jټ&?NUU޸X`qF}YKFL 99B$BHKKK)..q"|||,Ea^^CcDs}d6e=`ĉ,YĐ!v񥼼e)))$IJA"??z#|ii)5MS{ 9G;=ċÓۛիWSTTT\6mۉO>[hhG55 e˖!V5URyrss9sLhEEEP*4&Xec۷ln%`LL {}UU) z{ /@RRr-TTT;jkr$''UVV&T*YnrX&}Пڮ};yl.# raϞ=X,eڵp/Nh۶-'Nd޼yxyy1p@NۄMM!//WF'Dyyy{(,Z=|c4kl222Ilv7xG""w}7f)88N &%%KFDDpNN-ԦDAA$AnLb;4nMT*֭[oX7鉏?ǘ1c?>jG=vaÆձ`m2<쳑%7)))!$$~{|d::έgϞ){dd$#F[nn+񹨨E;{ƍ;v߀/}yѡ*))iPBAtt4ハOIĮ]l@pp0ǎ#11NYAxw0L,Zn NwM4ktkLL&n[oJMMtԋK@ՅH:#F0`tBddeJKK@Av*;v,={dŊ1f}X& V f֪Dpp0}?L&oRR`ZZ65'ϭM7$[\.*++[tRP85vGΝ/iF;uZՄRNaml6֭[nFoOH IDAT8ɰaj\ڵ ٌ sIke.\Vo<#{lX& @``0R!@ER1v:buu}CR%mLL 7tSE0?| AT2gGw>(L6N:Off9i]×z (0b4=h$\Ab+z1Aϻ:u,enn.ط4端r&{,ϝ;3gcZٜm۷]RZ vܶ񎡿zf$iy0͗?pw丯+]v.uJmۆ$I(J`ٲe./^`x۷/$55/~Аg^BBB5l7xcMV'T*PQQjwѣGeeeuϯIf믿RUU DFFvŎ'/dbҤIF"6XOS/00?\M=OO:p*++Q*޴Y6>>sm͟JbMdrdΜ95SȈf|βe˸DL_ݒkw\~rõٚT*MkCa~ݻޑr`0h4>sGo'%K &ȸq|I<6MQ3XRsQ* 2:fP !G]NZwwF)uZW.]E# /w%͕eHRRR?/={6Lիoi+**Q ɐ!CkaqFA\g4Q;Rzzcǎ\S{Cˡml dee!"VFCXXsΰ3p@Y}v̙mFYrSot+jCU봺SYYY}}}$Wj)Rܹ3]vTEEEVh4޽48w*@=s2qh45}gϞe۶ml6 ԩS̚5zodڵxxxP^^θq ;˖-["2.X۷O+K޽{ѣEGZ988Ag}TzMhhy~Nkk]tiOt ӧ44_~^xSN1c ~嗣vzrr_@W*Ջ#FЩSKN%iiideeFĉٳhg6W 4Fו-++#%%E !88ROR aÆ pݺu,^zҲ~ / WLEUqq1QCZͰaÈmP29F,, OOOKnСC5jӦMcܸqM-P(ܥR$d28}4'Ou P(HJJb.wXf =2ߎ'|2y}U!h݆mݻ_9V]vrNBiiӼ*IOā]7B%կw &۹s'qqqfryERXXHqqȧT* Ac̙Wmh4.]*/ef3=Ge,_{vOk"/ZnQF,c8}:yy?~'SwV~'V4Q\\LAAAXp? b֮]KbbbF;wf͚5ʻ;w2}t%x`44ν47F(m?6QrϪ*F#ڵCPTVVRUUEqq1EEEr-Jjeڵ,]wNbb"Ռ;~qeT?bg񡨨H_άh4Y^8~8})))=z4K.=x wy'kn}%ADE-G'00^OBB7pܸ` zjj`@V3{lf̘XqeYd'O-ߥs8ؗEZ7{n:Ğ={h۶-sΕ|o 1FôiXdƢ¢/iR4@67ڔ-nnnSNח3f8. [n[nr.oooѣ=z#Gp L&}_.}̙3/V7q=3.l6233`* A8wEEEQPP@ZZgΜiЮ/2SL{^{޽{3|p,YEeel7=,nCFjKtQҤI~I#((HzW\I$)--MRDDKYV}J꫍>4h )%%αb ):: ^}U)++K۷KݺuwԩN{9ڧl7j mVJ%AZz4yd?R@@CIK.***$I'=… }Vuw\q]#7j#%&&!@nݚ+-[L2LN 9gT*HW|A6RHs7$IM?DLUU* &!!۷7߸t4i/J[el‰':t(]v;{+NSEÇ<6OKFjԂBJbȐ!rZXr%'O&44ɓ'SYYŋ 3 [\ܶѬp@Վf0U8~8wh4:J%AAArOB={8~FqԄiusݧ۲V0] !$$PBBBvp)&No3<7̪oV/$ih쯙"{ ]n$HYJNͼm w7w&;+B$I#H—BZn0o(tZ]8>k,Y0[j-SgVM6tR֬Y?~<'N`++i0۔jBul^gjUKHi $ʦZygL4In5k8-..nYؑ#GXt)˗/~V++^wfxKUP DFFz^ `0Hr1Xk@}<G!Io7o͚5W_}2;;޽{9vAAA=#GRXPڵkߏ&˪z9*p5Aթ9d777n6ٻw/_|l"l*PLL _~DEEcRSS2f'\:N{Q 0=KVV䐓bPڷoO;ѣG9~dRπD2tZ'0cGA(-)D^ua8-hkq: )c.6;@챍/y!W`hL_u\u8ө_$"IENDB`(@ N212@?@535 {ONOfff)))%%%EEEedeppp878N<z\  FFFAAAIII.! !TSTDDD989///(((=<=CCCU```GGG*)*>tstKKKccccccUTULLLLKLTTTSSSdddbbbI212555IIIVVVHHH @@@ccc###  (DDDBBB@@@+++444gfg;<;%,+,^^^{{{NNN @@@ 222&&&>>>eee DCDٴ,,,w000TTTaaa212Bmmm999οaaa???qqq /./KKKv"!"mlmk$$$MLMaaaOOOgggBBBwww___lklT111???&WWW))),,, >=>}|}999 ƾ***%%%WWWMMM___BZZZxxxWvuvrrr###656% w~}~y$$$aaazrrr ***jjjzҿLLL{{{>>>wGfefCCCL#"#rKKK~~~&&&.Կuq6ݿqJJJ999<<<<<>>&&&###______ߦ@@@@@@qmmmvvvƒMMMeeeE슊jjj555ƫ=8=OOOɘNNNzOcjjjCCClllA#"#)()xxx󾾾]wiii _W*)*aaaBBB̒-wwwKKKccc4 mHHHnnnϕJJJyyyxxxRQRȱ\[\---kkk*```Ǿ666p---(((y῿xxxoPPPCCC?I111vFFFĂؿpwww翿\⿿ r 򿿿6ǿ鿿>CP]jwfB??????????(0`  202AAA-,-VEDEXWX.-.%%%,+,>=>\\\#"#? 'grRmxxxJKJYXYUUUbab  ?+*+BAB555333???JIJ:9:=<=!hghAAAmmmwww=<=X$#$$#$ONO*)*777\\\fffRRRYWY]@@@<===(((yyyiiiSSS;:;ccc777PPP~~~NMMMIwww eee^^^YYY$pop000SRSsrsyxy#>>>mmmPOP***323NNNUUUtttX|{|׼777666rrriii\\\췷999UUU666333CCCnzyzyyyNNN888?gggTTT2TTT)))˳ sssﯯ```DDDjjjuuuFnnndWWWoooxxx㫫>>>999gfg,hhh`` ~~~llla`aӺRRRHHHIII)շJSSSʬPPPzпI lll888nnn 5콽X -,-9QQQ333B IIIุyyy{AEDESSSlllUUU[$#$444  A ! !%+*+PPPeee```CCC$$$767QPQ///qqqcccAAAw B)))RQRBABwww[[[ '&'YYYYBBBHi656ihiNNNvvv___ iiiCBCʲEEEGGG$#$9'''a`aaaaddd###...ooo<<SRSbbbDDD+++jjj ) {Pjjjnnn2228KJKZQPQ[~~~SSS3OOOA󿿿OOOXXX췷666zsssH[[[kkkvvvnnnٶQ![#Ͼ]o𿿿XԿt%6bo|nH%??????><88p?( @ 7('(v???vG:3<<>>  &&&OOO  kGGGꆆvvv~~~zzz&%&|yyyrrr555  PNMNFFFmmmmmmkkk뗗~~~IIIHHHkjkZZZccc"""<<<ggg444 ?HHH  ccc///hhh槧LLL'WWWxxx:::wwwʤ_^_333 RUUUuuu&&&X{z{777===ggg㤤jjjCCC-nnnWWW888sssP~~~~r󿿿6pNz,??(  `b`jij)*)&WVW|||?>?^XXXGFG1HHHSSSgggWVW:::UUUkkkYYYꝝ\[\,,,dcdZZZ4444ŗޥnnnVVV# ɨ"""lll^^^bbb}IIIIII323]22]]999>>]]?2]?2i]?>RRR*>]?___]?]?>>>999llٓli]?>]<Pxxxٓllllll]?ll<RRRڐڐ999PEEElllll___999<P999EEEP0y(  pXKnyeXqcqdqdsaV~ZKn84= u84꣯uB/Z) kS/,7- u~iVdilzi\A4n{'/߀ L" TJ1`CH_DH`EHE4J.m [f:**@G'vC/tj6 hpFFC] O#8!ԀO:?aIv'*=;%^R \TeEg<@m"p$^LL\ԠmMqT2"zw2LDCmn[mk7жܒ؉Ե^Q}\^4=T:6igaŃ|pppcOBkPHR.N-kV[y\MIȇV4F.[$$A'Zptortoisehg-2.10/icons/24x24/0000755000076400007640000000000012235634575014603 5ustar stevestevetortoisehg-2.10/icons/24x24/actions/0000755000076400007640000000000012235634575016243 5ustar stevestevetortoisehg-2.10/icons/24x24/actions/hg-push.png0000664000076400007640000000210312100577421020305 0ustar stevestevePNG  IHDRw=sRGBbKGD pHYspMBtIME,eIDATH͕Mh\U}O&41ڀPM\h[qh""W](NAƅm.֯JՈOLI|$ޛw;.&2]x{ν&Ӈ {q\$zDxvҾ4.QxR׶vU @oYr#*H$ݑ* iY{2wdiYX^ boC$ Do|C8 xq@$H~qY՜Z.HĕX(g' HC֑r(16?š栾g?Oy;UJu +AȷY*! Ueک;L+G(8e/#K}߹7J(w&(>хu ;G׶i^}($Bc>I/ ű=΋^[nu?wvW/8(9-8qkN;m 5(IENDB`tortoisehg-2.10/icons/24x24/actions/hg-outgoing.png0000664000076400007640000000123012100577421021161 0ustar stevestevePNG  IHDRw=sRGBbKGD pHYspMBtIME!›IDATHkA?45DE 7/<(qȑ(lA"b1f .So'(w?m8PڱֳESH$x9^?Zq`A )^!5U-Y~ )V68鑄CW#ZӋ4dkDM6ߝҮtH$݆An?!F dՁyDkM9amcQ:Ng=߹C)07S$XbğqP9?.~yOϾ2ra&",yr648^.L\/M"y4@oS4w灉~AWYV܂IENDB`tortoisehg-2.10/icons/24x24/actions/hg-incoming.png0000664000076400007640000000071612100577421021141 0ustar stevestevePNG  IHDRw=sRGBbKGD pHYspMBtIME236NIDATH핽JAIJmmD<lT6F KA%FK!" " |! 1,Fr9w8w!)>jGr+EF wWp[ss#"AO+N25C2=lƃł/MaXy׫:"FdBŲlnME=AD JeHi00T(\"""./HĶ_tڴV'[*Ο$Z.|퍥`֟Gx*a}"EO"= Yk:hzo0-+VutzI:IENDB`tortoisehg-2.10/icons/24x24/actions/hg-pull.png0000664000076400007640000000211612100577421020306 0ustar stevestevePNG  IHDRw=sRGBbKGD pHYspMBtIME'{/IDATH͕[Lu3 , rDFWRC!(}` E411mblb|45b>Pi4Dmj ]²י9>PP@xz`VCs{'>BYe]j2oVLLṅf05٣"Ȳ:Twu]iXOLIFuV.uV`Y.s=ٿ}3Dw}cMe~"rLSfT|± &HG"-Ensz"i&]qwp{e3}|f.<^R`k| h6]B  i` 2BtrTH,j6ծT0zsA H|(\Tڣ JkC]:("ݦI4Fb w^Y |2CƵ nHӚ뚂6D$ Kn1ؙoFɤ!-">39"c.#-m7 )q0v&0bV(J$v/*A{Ett",,Gǡ{ayuVwc%L"'hRGRݲ/ާy|7Β6?㹣p?G"MIhT30XS8cu{y iXWyV5S'~Zí+-i4'(Wż5nTEApc~Dh^i0 #E{J41-ܨ0)ȝBB44\!`%@]"`ŸRI8SuA+q.yX)|3ĻtL89+ہ1yw_To.iEhjtLJ%gp_&l+!(XNcCU^U(,b01'_qQ/騵IENDB`tortoisehg-2.10/icons/hgB.ico0000664000076400007640000000344612100577421015210 0ustar stevesteve h&  (  c*\Ze!`WI{V֐Zf_bOĄSϊSϊaL]PDžK~N‚V֐d.XۓYޕbcR̉abf`HyTьffYޕYޕYޕ`PDžff)\ff`GxZaYޕYޕYޕ\暺]Z_eUԎI{f@f b]af^띢YޕYޕZcFv]蛣efb3WّZYޕYޕ_SϊPDžYޕN‚Qɇf f]WV֐SϊXۓYޕYޕYޕYޕ_GxWّ`a_OfK~LQɇV֐YޕYޕYޕYޕYޕ^I{_ퟭYޕ\e'OĄR̉ZYޕYޕYޕYޕYޕYޕYޕZQɇffVf*b@QɇYޕYޕYޕYޕYޕYޕYޕYޕYޕZXۓ`m[YޕYޕYޕYޕYޕYޕYޕYޕYޕUԎe;dS\YޕYޕYޕYޕYޕYޕYޕYޕYޕYޕf _YޕYޕYޕYޕYޕYޕYޕYޕXۓff;^YޕYޕYޕYޕYޕYޕ_f*fd^\\_etf AAAAAAAAAAAAAAAA(  HyOJ}HyN‚LTьK~KJ}DYޕ_UԎWّXۓ_V֐OĄQɇNR̉YޕZ]OĄFTь`fZN‚PDžPDž;ZYޕYޕ`I{Z\aJ}QɇV֐(Yޕc^Gx>\YޕZ]N‚N‚OĄK~?AmPDžPDž[YޕYޕ`HyZ`OĄGxZHyV֐ZYޕYޕYޕZOĄV֐YޕWّ!PDžZYޕYޕYޕYޕYޕYޕPDžI{K[YޕYޕYޕYޕYޕYޕZOĄPDž&\YޕYޕYޕYޕYޕYޕV֐\ WّZYޕYޕYޕYޕZPDž `WّkZ[[YޕXۓ!0AAAAAAAAAAA0Atortoisehg-2.10/icons/README.txt0000664000076400007640000000253612100577421015511 0ustar stevesteveSome of these icons originated from the TortoiseSVN project. We have modified many of them and added new icons of our own. All of them are licensed under the GPLv2. This software may be used and distributed according to the terms of the GNU General Public License version 2, incorporated herein by reference. Some of the icons used here are from the Tango Icon Theme. Some of them have been modified. reviewboard.png originated from the Review Board project, which is under the MIT license. Directory Structure ------------------- Icon files should be placed according to xdg-theme-like structure:: scalable/actions/*.svg ... icons for any size 24x24/actions/*.png ...... fine-tuned bitmap icons (24x24) *.ico .................... icons mainly used by shell extention or hgtk See also: http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html Icon Naming ----------- - Commonly-used icon should have the same name as xdg icons, so that it can be replaced by the system theme. e.g. `actions/document-new.svg`, `status/folder-open.svg` - Icon for Mercurial/TortoiseHg-specific operation should be prefixed by `hg-` or `thg-`, in order to avoid conflict with the system theme. e.g. `actions/hg-incoming.svg`, `actions/thg-sync.svg` See also: http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html tortoisehg-2.10/icons/refresh_overlays.ico0000664000076400007640000001373612100577421020075 0ustar stevesteve  6 h  6(  |IwEeqAl>h:ee8N'Q`MzHdtC.n?,j<df9d7_b;Y!_U Ke $e d7] g9e8`a#^#Lk CZWl <[ i;h9f%ec$d.fG^ dm>dl=ej&h&,!4N ex3 sB.qAo(m'. vƟ zH,wFs)er)e g ؐ-G eMd~Kev)u*_y$ !/JJj RNx+`w*g&.&w fUm" ["W!_w3y+bF^'N&h&Hc$_#`b's)`#NtDj&0g&pٰPp@ @ pPѰp(  wEtCQp@tl=vh;Se9OL{IwErBn?k=h:e8c7U +ROLi<g:e8d6+["X!U ] K$ Kh:]f8e8_#\"&Σ1 i;g:e&b$_" LǕ. Kl=k<j;h&Sf%0&A`t o?m>Qk'vj& 6 Qgu sCqAto(tm' gz4 xFvEvr)Qq( x߉؞ }JzHSu*t)r( M шӗ'B KQNLv*u* %AT Rx,x*v*Y*t l L["]X!Vy+,x+};_&S?e%a$^#["+y+As9U L?i&h&d%`#v*h&Z"LutDk';  ;( @ uJ wEEvEsBqAo?m>k=i;h:e8AmI$}M ~Jp{HyGwFuDsCqAo@m>l=j;h:f8e7|`7 ONML}J{IxFwEMuC qBqDp?"m?Kk=j<h:f9d7c7PZRPOMKH@h<Ch;f9d8c7f3f3U T RPu@f3h;lg9e8c6i<X!W!U T>  U  T i:Dg:e8c7["PZ"X!W!D Q - Pj;>h:f8e8N`" ]#ߔ\"Z"lm$ '͝E]  m$j;uh:f9e5 `#|_#]#Ιf!-Od @j<h:f9pm$b$a$_"C R ܼXl! Pl=Hk<i;j5e&Ad$b$ހ@  cvk<j<Eg%f%e% V!=ؙ l=i&g&f%K4 /I q@Mp@n?k&i&h%" -G7P !<  rC rBp@l'k'i% :+E ! wFtDrBn'l'j(  !1L xDwEuDp(n(m' @XYn {I"yGwEq)p(o'N Sh' }JK{IyGr)r)q(~ V q؂xщ UL~J|It*Et)r)ضm$  ϐڞ/ @NހL~LAz)u*t)s(U} R ޗդʖ QQCPNI$w)jv*t)Ҫ+! Lc#MSΆQP|{&w*v*u)hm$! !m$V lT SߊS x,\x*v*u*D} R   RX!DX!V VPy+x*w*v*1Z!im_!~ V  V ]!>["Y!X!f3y+x*w*u*uh+t]"W ҆R@`#u^#\"["f3{,z+x+w*Ƒ[l1Z"S҃Oe%Jc$Ĝa$`#^#Zy+I?ϟr˚lh0V PҀLi&?g&Тf%d%c$`#Nv*o(h&a$Z"S LzGi&;i&h&f&pe$ z+s)l'd$]"V O{ItDk'Pf)??sq999999qs?tortoisehg-2.10/icons/general.ico0000664000076400007640000001525612100577421016127 0ustar stevesteveh6 h   ( :|w\QD\y~Y3WjM]hQaProv,~djl.y[158āÍȋɒZadӛס߰------------------------------------------$-----------$( ----------%(---------- &( ---------&(------#)"-------!,+'  ---------)* --------- --------------------------- ----------------------------------------------------(  6sur-%>-Z6D0]&f N4a!`  T8dV '# FNH83f R_K,gSUN"ɒסȋPdy~ZH3ÌӛŃlL-=<}A;rע߰_٘SĀjwn+&#@xOCݺ`D&#@# < (=䱨Q@94h 6䮦MA1 .8<0/+ P =1, _ ( @ @@@@++ 66(>;5860871=;466(333<;4FF?=<564.971>=7@++ <;5pqmDC=55.H55,:982:82999 +++=<5deaHGA64.^55/R;93<93@++ <:4Z[VNMGIHCJJD><7;:5QQLqrn=;655+<:4JIDbc^>;6<--:81rDC<5@@ 74/]?>8LKF>;6UUU74-J<;5DC=;:462.B;:4?=8:83i55-D=;5<;5660P73/F=<5<:4440;55+><5@?:=<7?>8=<6><6<:5771*<94=:5uxstvr<:372,.65-BA:þ<;5HGB=;655-D66.!=;4A?9mpmoroDC>Z[VBA<860_970prm@?9[]Xab^WWR:82+++@?:<;5><6BA;npk971PPJ73/F=;5mni=;553.i><7RRLbc_55-`>=6;:433375.64.NMG>=777)%??9>=776/>=7vxt<:4^^X@>983.286177/YZT<:5=;5::4999=;5?=733/h333 =;6PPJvxr<;564-74/<949729//1????`????tortoisehg-2.10/icons/menucommit.ico0000664000076400007640000001373612100577421016670 0ustar stevesteve  6 h  F( @ N JP@NO-ONL% NORM]On V^PO^jQNXPfojPL/Ojr uhPQ JOlt#x&{bP@N NPh!v%z)~+׀^QL%UONP^#w'|+׀.ۄ2߇oPPYOTNP6Q#v(},؁0݆48.ց SQF fPSdNP_(}-ق1ކ59=5`POg@M O UieN QPq,ف0݆48:84&x WOP XnkeNOQP"t/ۄ2߈5652߇.ۄ+׀lasnjeNNKP"s/܅1އ2߈1އ/܄,؁)~%z!vrmieNM<Ph-ق.ۄ.ڃ,؁)~&{#xtplheNN$P V"t*(}&{$x!urnjgeNMcP`#x#x!urolhgeNM(PN\sroliggeNIMYPO[kpnljgeeedNOQNNNNNNNNNNNNNNN????|?8??????????(  |(|(<|(2|(|(<|(|(:61އ-ڃ)~%z!vrm]PxxP#Q$t8<840݆,ف(}!ucOP#xxP# TQNNNNNNPQP#xx|B~|LxtrSĆOefPēxpdB|@gnnnni~GxdBĿdnnnnnPd@䲏xಏxಏxಏxಏxrSdBnnnng~nnni~t ĿwnnP|@i~QĆzfÕ|B~I??000000p(  lll>>>|Ti|T|T]?>>|T~~iii>*____________EEEHH~~~]?~$m~]?____________lll___ll______lllll~lll(  lll>>3 |Tg|TtNR>++|T~ziitN_________EEEH<~~c|GҾc_________{{{mt?ccclu:󗡡___lpxlu`(olltortoisehg-2.10/icons/menurevisiongraph.ico0000664000076400007640000000344612100577421020255 0ustar stevesteve h&  (  @_?_?_?_?_?T|EEE_?_?_?_?RRRT|i?]_?_?_?_?RRRT|~l?]_?_?_?_?RRRT|~lT|_?_?EEET|~T|_??_?___999999___*>T|?_?_xxxǍOǍOnjJڐT|?]?RRRǍOǍOǍO___J|OҺ___ڐ999ǑZƐYڐ999___xxxڐ999lllڐlllxxxxxxlll999999lll`A ?(  peF_?h{efuu$/h,HVgGeElLry6qrl6+VjsPshIpRtiIxvtl6AYd t5(gjKhLwlDYaeI9\ErNڲdkMj[SB>9SSJ`he YM:~fZt?P~lP|E԰rnpq >VWytEh[dFzLČQպֳ}zuwnWIۻx?Ą Ȉ@Ǐ1?|(  ))" : 4S3DO@VLD^?sn_1ZTTy^ºUp@"zwwzQQIhNuWMwWt̯tx( @         "#$$&-"%#!$"&&-(,(+)%0((')(.+,65!20,6%>3)031 >!:/'>$'=):84%A)A-B;44>3H=2?>4S=1X>.I(8D+=B4LA6L*RB4MA=6H1,H6TC;L3[F3GH6S+ Q/CFDXF?V(+L<1O0PJ<KID8P5VIEMLAbK@[-OMIYNCUNGQQEfPDSOR^6eTFUUJe1GYBVWE d6lUIlWDbVKUWVF_DeYNVYW-cD>d?g[Pb[SmZS]^L^\X_`Nd_Pk_TMfJbaVnaVEl?t<cdQnHjdV+oMfgTweWqeZrd`|<9qRoi[FuBui^{h`xMgihAHvKIxFzlhzncqtYGog~rgorpuvcux]H{tlsovk~yc~voyzg"Vvrz}byrznx{y~f~sd~wVUorm{[ZpwR_^-d6jyuru`bdyfhkmovv1xB}+p=z}}N0|H_ǕEΌš¾ǰ̩ѡiس B@{C FqeJљj %a Y}T,ͺ<3\Π) u@gw\E&_I02̌R+![rLLxV 8,ؙF5vnrȦln :S}2"X'V9 ۦPXZA;46'*h.uyX¶k`AO7;S]ɾh(GʰjeZQ=Hv^Ԯ.—ee`H`rÅވK ʧ}||}l^fШŰcd?M귴*^#狟D?|(     !-))+5*&//&8@&F93::5E(7D.AD5/D<XD70M00P0LM=U6QLA1U<^QDdQF9Z8"[?ZVGbTM`WSgYQU`G]]]/iLfc\pc\FpGgiUxf]tf_liXdh`|hZso\.zOur]/}Prt\"Q|smvy_zxc L|yd{{flb}h.]k?fz~~YZ{y__:n/nōǰ I:-7 1$J+96;8=.0%B "3XN?* (SETUG4&!)C,V'ARWMFKP@L5#OQHD2 xPNG  IHDR>a IDATxw]U?s}=BR(!A@Q!*P|과 *WD " RHInoϹ c}>g9,9PATPATPATPATPATPATPATPATPATPATPATPATPATPATP1捾 ^D8G:hp7p0 x+0x܋~1ۀZQ0U55ܓ0"TՅϾwU,!T}N~.\eYa\uꛚ1~,-;w}lYFzcn Ixy\Dz1'ӟw^eYXř8~Svv`|{#ӏ;_)oE!"bYWDdQ:VYE?]0~4◄A@1'N_DÏFDdUD "="򘈬,keY-"VD "?o"2SD䍾DOQFe?bٗ^Wn}R|ٗVw_ē7y'\&" ?M>A)`cR.;vl8gdtR3}t* >F[|:[DI'u2^|Ig2O1+6+o5y3+63?zIe2fQS `OV?iWJt5R"2X&" ܹs)m&M\) A-[j*'|\.fJ1n7ߐM2_477[Wܪ25vE1؉o`w?.C6f nogߎ 7 D EoYwy,^X)0 ~D#Ikp@Djj88g?_+~Oz{{oQJ1u_kjWܾB6P ٳ3gqlt:JArp^y(Zw*u3.,:?"D"awe]ƢEZkB| : ÐPkty3hJP, ˊJ),5k:_ZnJ)~/cLxx\x;Ε/+q#Fqhwwtv$vA ^8(cJY"r#(N+2bPa  Hu#Xeamc[M2 LLڜ\xхڧͬ[81fkx"J>fxG/R dk9|Ȉw`vY }M?HZD>e۶'??qU_@8KG} ? CM[Mg(H tt*I&!NJ={6+Wn/|sc"r p1yrDwϞ3,<~@<#WJ1l"?2,>D)uϖ>?)Jk',^n)S)~E DJj]:tf|eӯ,mG NPU2{wO|_O"r1|W칁ܷ>nRx\s5_誏Rexm--L9q2dpn!D3l,ռW |믿=Ayx8:88x~~> TjpQHd 6dL:EUUljjʐfO~ʼy~RFDr3N;4|)@/P 1eՇtL7޾ѓ&ijr}}`>g_nf?x1VcLWoHt׿477͹S,Rt"Ţr9:{#m~@ב0JA`j!QJ\ w ;\Au,55PSWL,]v()>!"ˌ1G1ԙMMMz*((|=G(P>o[A*MGuh ?)@3}/7pضKPP,"y}>q͌Da >,(*Sւ%Ga (%EsdL:MUUry<%KOR,;@k"r1yH:m0'$0^E$ 5w?΍,Z\ñs&@0nABUUښjijgHcuMZƘ_pb^CD>͂ .v>N{b:?$4F&̘Q[p?[(az;Svς,JOzG5 "CR+Edw!sNYN!_( E\׋ Xf L!iQe7z+XIGF)YX ;0@$m"LPP[[MSC=C62lhM 8N;UsB`ߺ _՗MGmy~wіk3IoqZvs+cNd,z"6 t 4T:XR.sݟ֚gL' Bv=}}>SdƘEwyGQnB@>_踃xTq:%MayH負%JDAD=q#  #NSO#[]ɏ9DuW}l]D";5bkH|#1Mn HTR8q81;(Pk q}I /~P>NsHPNAqDe Gہٸ~5]{AQbHڂ<vb"9 k#7P( Nz pR,Ws~*D /[&"BW/]T3/}헯 W۶=?̚7+~_ ϥ^bc~@GD(B,,SK,ͼ@D)~u=@aDڄ (,0}Ċd"ʢ==1WWJEg$ ,Q%H &m?yHt"e0ӏ?ΐ!CwA*E"iEU"Q{xtwm6ںڢiγ)WKG m4 t䪔¸>Fۡ]@π$oղ_i/lU|WD&ir1oWiG~_&H ~ǥq] h3,:QStPb *K ||7q!& I >iac}M"ac[ N ?vJvdB$Ȥ `<5[ͻ_TRRU?YI;aps9Yr#sBUGSD\eu Uluh`pR/ƘƘC>Swݨy0Aǣd#PJ.8q>3Uѥ~+J蘊=;r(; rBˮ”\F) 5S JF AInPgMEF%`@R!I%d2lڴ5g&<# C@"~׸1@htY~:8,+Dغ$6cf "<~bϙ=,&C`Yn /`b 1!9wsrR-uՍennl#q'[x7{6\p "p;pp饗R2F~7uedMYѳeuD;p?됭犌9wdԹwJj!Yv=AcDJ ZlX˲0}zhÖkdL;vk) _Ƅ1ߛ4xjYҵiՉ#.m;{>ɪw||INsX?1"2߯1ǴsrIeTɍ Pcdϝ佳|AK( ssrAE#,a;Z,Krd2n"刊<"ꢺ&Vvni=J)mhllRJG(@Edgvl6X°ͳmO`qx_|>OqȬ8L%=9zt0uM֦c-j4Qcxn=---478t3aT N}h4 LL}|ϥe}}9X`: ٵe f8?G1() 2u+ѶZ}w}<#C6M$3gTJ}/0LFOl/"Ee=<1"r6G Ԭ,5nqd:n-ֵt2CMLѱCK)~tM;Xbx\rM&SXJX`}`(\.iqIZSbgL M+(`A:8mۆRG۱lmnMw[7vVH%;~~Xy}'zԝOH%?ny|w,T^5h-ȟ/<Ҷ0=,Qdͭ-{Q(ʕ+Yru.dc6||3w$DT&C2DY6RY_W6q6ɔL(P@c]E eE=]NK)vIƢcqÎjCNR` ʨXcR+&B?,~{Xw:RC'}%_u嚿_E8L֊\;٥jQB5߁cDС65Q][K_W8kyGx衇V02e*:YDJgH-LBS\I/}P9# N v54jmCGQj[  +<צ@V>)7ߵ c40ЈcĔyFf_ӇAn?tb}øw^q;x-1Q}n?7v0 ECyKN&[?򅙪*i\@_g'VG5O1}t&MJ*"J֯T G*E2Sa=76aѤ)Z[[i9F8iki-i2lcA`+B~v{n33 "p/_xͅzzP «^IC9?w.޷clj;m6)f1nd X,o@)mt9'ٳͷe:poeP`)6@X3J)]fy^p> q@!C(sĊTTT%>J$ T% u-EgG'b%'/r߲s'aSBE`tA=-]^1tП]~Y.ѵmkW|fggt^ws۾˲He2;3n2UY#qma $iR \uzrm_$8ؿ1 ЁCD x|05GbH$H%td2$ ߵÙwIeN*I*{k7:9܄c]٪Wv[6˗FE׾x$Ja.5FGDՏHƦCq ꮵ MyR]_Ѹ&, ˶I$3Ux}ʦkIgҼ9ᤥQ )R,x8N1fc~Լ{^;ww~A4Uv&[]GMm-U, &-%q:x\.G>w^uSgΊKP$ dQ8i~ЗG0WP,:#2cH5 0ӽˁeی?iRP(m$RfTUnp9ѣpE0n$ BO?ϮZfhGz@_j ҩb?:+f4QʶTm^@_|_W!ҳ?l;c mtTtgV;"?ر?acƌaԨ74џ6izG`ʜc6guMN=lǡ}8w/me.FbQ Z[[eڜ|#}~v0vq' P6G)v!Bh sͯiH J,@ZDV+Kf}ʸ^Pws.v{4M [8 NӲRq\dTW>+w6жT%뮮 `>d2zB<٣<+T"?uܺڴ|nT:ͼ%KXsHRQ8hٱq#u瑨#A;0@/Fk$\3/D~\L]\ \7ڊ"~s)k -, 7d*EQqHIRHe2HӄAٿkTTSSWБ#0cFԳ'wFuSZ3( #*JI?DPUvaA*%LF]0[n}R5T"e+ұc> ,cO b<\ *>EVoQU]m/Y"#O \0IQ|t ;X(ɠjB1AQ)c9bl ,kpmێx ֈn5|%0ޮ.F,RMH7zZ2 f5RS]ǃ?ڥ~cL.WWΗa < bOE߁q;-7*+D3y2.^L"&xA,;aH& TD&%XpDQ?aaPq43&d%e Jv?UYŽʡt0swSVQWWax Y6|U+KDdE +qQ$ZBӺ9Ǿ9ygC!u#5=4A ﺧݾw3at:{6 (\۶T),QA&+ v*+r)ATʹPJPB,udQcʴ9=U!ƞ ـlf0|6=2m^)"7k/:`c baai ؚ&[}Vf]EԎ6! F1=B6,z~f>f6w7.ԨrVnV̙ joAB1I'zWǙHyAv1J$I2^Ba0T!,dCM! w|b6]|HaOE _)^(KqTx ugmlW04Mf!aXi5|cScMMMk{]|`޳ݛgԩ4!ÇM"MlxpݴOx5R}$j'Ivhd,bQ 3QJRʊADhݽq2b|!t8UH&HV v L$?mX("_4,cqG7MO=G_Z_`wWh<6lذQF~*ҾD2bLUqܷwDBR*aܔ)4f4A&\ץ=۶b>/F4UC4ͳzX$&H bv(k]?Mtb-ͫ+˱#GVţ<௭@p8\0vSSSo__TUUR8a2Ynkx^U RAٶ~}yY7FМeOa&m4AA= 1*( 4E@hTƼ."RDP;^Bo3lf[СNP3 b2CC 93La"=yyb[~c_O>r\n^K!=T 0fHQ^< ѡhA%C)O>5wal2]wuu5hm0BԙepQd=s M6ATʳ,1qӨ*ˏfDhjCkt7WU7m0, u QeH>Vظ"Ӛ'g:N9l,W]z6F&>p8x5KKUBKk 9Գ) S޺kg 1 q3PR-j׮1D>.5 UzFhhtHv (_Jd)Qֆ :4n'Iڏ'&0!U :!Q-+5/9߹{2e15s.S?ol TDAC_ rŧ~ν{gl޼9ZRՐme[  #F JsKشn5xdNxnQE/H1t, eۼR5 0y,D-{Xz1dc_|cފ1+1䏱Ӛ&IW$3dJa%\_l`1b 5tlC;Y[83b'#E г[_j3(vaGǜ'{OzC!њ!xrV [Z>C:lum`ˎ)%W"[]C_oPԣNj 1c %zu J=Px9X::} k~r߿lUHSG͢%g_5lc” iEE(B;2b",>m{T ڵkYK7 \eU˰Agʟqb+ uMG:ѻR4~k?4\Zk˸m(e>֧Dh+='xǍGΖf4_3z|䇯5s~OƱp?GfѩUs6o&`8{2FDEc^xūBw;S2tdZwgo~@~gH<~1f×{zeם$Fy8^Njf/~v"7C~D pe78df?O\w3=\uޱw9#_cw>ؼ/zcVKEmَgnCGbߕ~# |~v_MD-}ˎR \j[TN&x3Ϲ?66lNUHOgWk>6?r6CFOf蘣ؿm-mO[L&PBw /A.3IJEݐdN;45"1cQzKx1YI"j1Ad7cg/ * * * * * * * * * * * * * * *x?^jʃIENDB`(@ +[  v62OG+/lT⷏⻗ٶ qV>z3>0"   Mz F3Oʼn@ǃ+oJPC>տлylsaPr]JmR*!)=vʭMC:v`LjXG1( =) =.oVΒC‚0rg î -(%Ĭ2,&u v9S3gS,{ѻXOG}pcycXM=cnަ[іHņ5w"gU  &!Z.HB v㸐VE3\I7$g8|%jIM?:QHAï`WM&"boע]ΔLƈ:{'k[QT+:0-θ8:OOD!"ǯ`SG A'Aƃ/q h)wh^1*&fZQk`U&+K:^RH{:s*i\NKJX-C" .L8u;H}@=/-йo`RN+Jˊ8y(lbX.8P([.,x**.DD5 6  >jRA}^8|Z0X%R~J u>q<o: k9 v? *(% :G$X/b3G;   |F>8+'# UՔA1s!fYQSQo944/ 440>>5# BA7NM@.+$#@/(V>$`@]:X3 S.D'+$#32)  2c3HM JxkYϓJLj:{+nc[0U,}As<881*HG% % W-#V%Z?jY˒Mlj=|-qrCP [2#0.'{yets`xvcn}}n~~{yz|xx" %rK3 K:l)ThAC*DB8wvbus`}}gv9:.523HDB,*) II;kk]]K"" FE8zED9RF,RF) 0d #M72$2.(lkY}|g}|fp[ZJ{y||rmukfpf`LE@'!   '#!=86YUS`]\KJJUUEl "NC'  ee96-^]MjroruLJH}ysxmfl`ZaUNXKDNB;K>7E81@3,;.'7+%6,';1,PHD643%%% }~! DeC0H0' RذMM>da`|z~|vtmxjbo`WbSKVG>K;3F7/F=7GB=>72C6/A.$VE<.+)++*0//TTD}LJ>7S5O~NTTZZJsJ(=(  Y.&dbPm>>2 HGFlihxtr}vvo}nfwf^q_ViVL^K?[RKUH@R=2[G15.+?>=RQP vua%4#WWbbbb^^XXWW3O35eEOOd:9%GGIddd~wvo}pgyi`saXtaViSHfNBcJ<`MA`I6\?0YC6YLF|vs~|AA4rq]IRG}}vvooeeVVFuFGoGPPNNPP7W7ZHpZ9eO G,,,pg`yxqqj{kcNF@wg^VLDjZQfOCdK>kTHHA:aG8aI=^LBujd̶ά̠ǖĐggXXL~LGxGGyG&@&   t#!Bqg_}wvophWNHxkcdXPcWNkVKiSGoZPGA;gOBgPDfRGo_UHGFkRRCٸӭ–^^QQ&@&  (=\=@a@@b@.F.' ~q sia{uwnglb[aXQMD=wldzmdIC=vd[o[Pf[SD>:r^SlXMnZOr`Vw 998_b_ܽÚ^^OO/J/K~KTTZZXXTTQQ%  LPOOO{qh}zrktjd|mesh`TNIWQLaXQ`WPvg^r`VsaXwg]}uļZYZpp[Ƣ$$+++ص__QQOORR__ggee[[NN-J- ¿xo|uqgavorjogphzjayi`xh_yi`zkbskeeSn ?***̟xxbb\\ZZiijjddWWGvG/b/wne^~x{twpunskqjqjsm|u"!!6byѩww&%" EmE>d>@i@ !5!9www{sm~{~x}w~x|776U 9:9ٹɕ#5$uۮجTTC31(>>>Q)&ڭԩѦDE7֪23(%@񖖗ý+stwx_ҧ~1119:9fqfyӨ`aN›##t@@@qppiҧklVժfF򉋉NO>ԩͣlmVϥ#$$$ ̢ҧ٭VWEp//%"&gRRAllV//&zk?L?  ??@?(0` X o < X-  })U4=%wà{L:* z $xdT@6-_N?;/%' IsM΍2wO|ndҽUMEuet1)!(Thz\# )VT3ҷncWvhi]P37MnmQˎ7ycZ3&F>7sfGC#q:BR<{H9+8, nfC+pyFydXPti93-!KhlԟTƌ=|%i WU:t)GJHbKѼ@93)!9 ,,xQ7xc^;2")H28' NnFn8g%]MDBg5  (#`2}A Z2k\S2,(wkLC:!  /sQCĂ-p\LL~@ KKB  (("A@5+1(9*'X?"dB_9 W0 E' -#/.&%A"x=A e7G=8-eHNȋ9z%j k;Y-v>&&!S#"KJ=zydponigU@=3/+&,)#50)MI>sp]lp~}h*& %w<#fM4-&QGƅ1sI p;J(NMArq^kyv~ursw}zvua &$uL!3%L3frHF*!*!feTwvbo}BC6762NKF861>?3iiPO@++&*)$i<9/L@& U  #?.'HD:{yekrsr^/.+wruke]SN/)& *$"E@>^ZY@?@))#yts`E?% $ @@ vu_iw~}fGGc?7W7+C+-q,()zf~}e66,?>;a^]{yzwp{ldraYgUK[J?TJCP>4RB8L<2U?140-DCC--&GM=O}Naa^^SSFpF# Dff n4Q\hA WVX||||xq}ogvf]p_UiSHdM@^J@[F:VD9W;,D93b`^)))}XVH\[ooppkkccZZ 2  U  vF8/TA7leadccddRzxcco_~~xxqqggYY%<%'>',F,*C*#   Ho>}]0%  *;;;xq|wvpogUMFrbY^QIfOCdK=WJ@^F8_H=h[T0/'//(βˡƓrrYYJ|JI|I0R0"z Bm~x{ztsk\SLuh_aWOlXMkUIXMEdODhRGjWMZYUwiiU޿ЩppMM($@d@EkE7U7"2"*$$$|vwpyoie]VvkclaYcXQcWNYQKn_Uo\Qr_Uwo`aQJJ=)*(VXVљӯmmBkB_<2N1 #T#CRRR{ۻSoRPN?e lýhhglDfs\ɠti;;0?@2^^KY ܯ%%%8+,+u{hѦLM=[[H>>1dePL###aaa{{{䅅~ffe+++NHbdcwymˡlg56+$%hiS  C ׫}xy`,,# opYUUD'(),  ????( @ @ |_l/ L5!yL7P8׷xfUYF4gQ/MeLxgYL)L;i<| Jb[Qtvi|5lVD," j 7}^8ofl\TWMEF":.cǔC~ dT L*6>2Jd3|=~p<3+ I΋%lt;@- &#,G7+cFoF d6 R,5 #'d4V+#@,B;5IO7xM[.="+*%WVHlusb_OMI>PK@ieUnsTRD J&.eYA/H#U Q->?4{zevccQbbT__Oqp_^NeeS,'# .=B6njYsxwaOLHyspd^C:5$"1)&TNK,+,ggTQPC:D+  7@@2 򹳔iMLARPPyrxh`dTJRC;LA9G<5S=010.883m?d>_`JuJ'='Eff t<)t+oNRORzs|mel^VkTHWG>VG>V>0KATB6[H=hgTUUGͨŒ}}]]IxIFtF&>& /}tm|qj`VOuh`cULmZOWIAjUJtl^]VfjgHwHJwJJuJ2O2  r |{~wymfqe_laYrd\ve[vmÿ^]JVVE8WYWZZ^^jjUU###vp~wxqwqWWVѢH_C|zb5H1$qſIHHprpȮut{}cvw_'122{{zmw||cVWE[9gvb,)))z{aƝrs\f&KGG9%% h?! ?|?(  )8>ZVG^QD#8.$:nlb|hZ  T7D.w K.].zO1U<"?f L@&-/}PQLA/iL/nE(liX{{fzxc}h|ydk!U6Jso\ur]fc\bTM5*&F93::5U`G9Z8D"[?"Q/D image/svg+xml Remove 2008-05-07 Peer Sommerlund Remove icon for TortoiseHg tortoisehg-2.10/icons/svg/log.svg0000664000076400007640000002246412100577421016116 0ustar stevesteve image/svg+xml Log 2008-05-19 Peer Sommerlund Log icon for TortoiseHg tortoisehg-2.10/icons/svg/sync.svg0000664000076400007640000001207212100577421016303 0ustar stevesteve image/svg+xml Sync 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-2.10/icons/svg/checkout.svg0000664000076400007640000002355312100577421017142 0ustar stevesteve image/svg+xml Checkout 2008-05-24 Peer Sommerlund Icon for TortoiseHg dialog "Update" tortoisehg-2.10/icons/svg/proxy.svg0000664000076400007640000003464112100577421016516 0ustar stevesteve image/svg+xml Proxy 2008-04-19 Peer Sommerlund Icon for TortoiseHg dialog "Web Server" image/svg+xml tortoisehg-2.10/icons/svg/refresh_overlays.svg0000664000076400007640000001444012100577421020712 0ustar stevesteve image/svg+xml Refresh Overlay Icons 2009-05-31 Peer Sommerlund TortoiseHg menu icon for refreshing overlay icons tortoisehg-2.10/icons/svg/merge.svg0000664000076400007640000001572612100577421016437 0ustar stevesteve image/svg+xml Merge 2008-04-26 Peer Sommerlund Merge icon for TortoiseHg- tortoisehg-2.10/icons/svg/detect_rename.svg0000664000076400007640000004723512100577421020137 0ustar stevesteve image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-2.10/icons/svg/ignore.svg0000664000076400007640000001613312100577421016614 0ustar stevesteve image/svg+xml Ignore 2009-02-28 Peer Sommerlund "Ignore" icon for TortoiseHg tortoisehg-2.10/icons/svg/recovery.svg0000664000076400007640000003076412100577421017175 0ustar stevesteve image/svg+xml Recovery 2008-05-26 Peer Sommerlund Icon for TortoiseHg dialog "Recovery" tortoisehg-2.10/icons/svg/shelve.svg0000664000076400007640000002505012100577421016615 0ustar stevesteve image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-2.10/icons/svg/clone.svg0000664000076400007640000002030212100577421016422 0ustar stevesteve image/svg+xml Clone 2008-03-02 Peer Sommerlund Icon for TortoiseHg dialog "Clone" tortoisehg-2.10/icons/svg/repobrowse.svg0000664000076400007640000003024012100577421017513 0ustar stevesteve image/svg+xml Repo Browse 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/svg/thg_logo.svg0000644000076400007640000012624012110205645017127 0ustar stevesteve image/svg+xml TortoiseHg 2007-dec-11 Peer Sommerlund Closely resembles TortoiseSVN logo Hg tortoisehg-2.10/icons/svg/add.svg0000664000076400007640000001237412100577421016064 0ustar stevesteve image/svg+xml Add 2008-04-09 Peer Sommerlund Add icon for TortoiseHg tortoisehg-2.10/icons/svg/commit.svg0000664000076400007640000001121012100577421016610 0ustar stevesteve image/svg+xml Commit 2008-04-09 Peer Sommerlund Commit icon for TortoiseHg tortoisehg-2.10/icons/svg/init.svg0000664000076400007640000002272512100577421016300 0ustar stevesteve image/svg+xml Initialize 2008-12-27 Peer Sommerlund Icon for TortoiseHg dialog "Init" tortoisehg-2.10/icons/menublame.ico0000664000076400007640000000344612100577421016455 0ustar stevesteve h&  (  @lll>>>T|EEE|Ti|T|T]?>>RRRT|i?]|T~~iii>*RRRT|~l?]EEEHH~~~]?RRRT|~lT|~$m~]?EEET|~T|lll999999___*>T|___llڐڐT|?]lllll~___l___ڐ999ڐll___xxxڐ999lllڐlllxxxxxxlll999999lll?(  pXbVgF;8*JGQJptUok Z sl_&eXRz{)Uj wLPu_:;ޏ4Õq*vcV~||2Te s?'ps0խ\'[NZ|y5LTqB=a#n0zn~s^paEzth 5>GX>leoyty̯jhc.]qO|RsezOĠT]\yKĽ,44~ʈyΓJ }/-,J J wXJ ijjQC9*J 000qJ o\NddcJ J ~esfCJ ȿqJ QJ `㺪u侰ɷ̻hhٍq{OwQdu캧㴣uνm~QvÊu݋nuo칧ްv§laG[F݌puo㻬vźɹڱ徰ʹwIpFggkuƵٯῲ^rodۂbôƴ~}sobg }ݦu⢋u䛀ulq|w 齭ڿ⮛u멑먐⡊uڎsw럃 tortoisehg-2.10/icons/proxy.ico0000664000076400007640000001525612100577421015673 0ustar stevesteve  6h hF( @ 64. 64.Z64.64.64.64.64.64.64.64.64.X64. 64.64.64.@<0UN3e\6`b:th8sh8od7e\6UN3?<064.64.64.64.g64.IE1lb7q:q:X~G!c}p:q:q:q:q:q:ka7ID164.64.c64.64.<9/h^6q:q:q:vp<jqtn;q:q:q:q:q:q:q:g]6;9/64.64.64.64.C?0wk9q:q;r;r;=Yxsvn;q:q:q:q:q:q:q:q:vj9B?064.64.64.D@1|o;t=t>u>u?jxEy|!nrs<r;q:q:q:q:q:q:fn=2B464.c64.64.lcr<q:q:q:q:q:LM[64.64.64.LH4{F|H~IKLL.sȂȂȁ&x~|J}H{FzDxBv@s=q;q:q:~p:%ap4K864.64. 64.tkB~JLNOPb[!ʃ!ʃ!ʂ ʂ ɂ7m~LK}H{FyCv@t=r;q:SIqq)R64.64. 64._B?2KNPRTT0u"̄"̄"̄"̃"˃!˃_\NL~J|GyDv@iwDFUrrqj:;064.X64.\Wdx'Z64.64.64.n6>4.z+ֈ,؉-ي-ڊ.ڊ.ڊ-ڊ,ى@t`\Uh#̈́"̃ ʂȂƁĀ!n6:164.g64.6L;,*ֈ+׉,؉,؉,؉,؉+׉ei]Y1sJg3s ɂǁŀx4F864.64. 64.5O=**Ո*ֈ*ֈ*ֈ*ֈ8y^ZV_WrTBhȂƁw2L<64.64.64. 64.5E8,p(ԇ(ԇ)҆;vz`_ƾ\ThY-v"}ǁ$j4B664.64.64.n64.4\DTWYZZYcURPy~N!ŀ(h0]E64.64.j64.64.64.DB5`[AtnI~wM|N{L{sHphC[V;9?364.64.64.64.64.`64.64.64.64.64.64.64.64.64._64. ??( kmo!l#x s"~ >o>5z7   *0 nxn,5 &1MTww.8($2-:2" ) /'-)1!7'# 6"8IS2$;1LSW\^b.I";2K9&ALY5P,I"<0M&B|9Y3Q>U-K:Z[q6W`pmyÆ>aEj:^=cVvFl>dXzeڂJtFrِJtQUTycbk}jiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii/iiiiiiii0>)@2,iiiii7EN? HH@&iiiDHRX1 -XR-iiiHR\[(,+LU* iiWOZM6 'Ua' iiWVF#8TcbL$iiYO!9HD^hfd: iii="@NVGegd= iii%"2d 6lCY 7Z1QBiTU)&'>aKt!7 nXb*F=cQEj.I2-Mwb.  x`f 2QHr:Z)(2Keڂ}k>b } ed9\>a"93U:Wj&A  e_?d{,I %@;_Fr[ql.L aRBi++ 85PFlJqyc3Q*Y7  .!93Q<[Xz"849Y;] Bzt *1.I0M-J0'JtHrv &>.   ?d1Mg,G5 $; s!s5tortoisehg-2.10/icons/menuhelp.ico0000664000076400007640000000344612100577421016325 0ustar stevesteve h&  (  @|T|T|T|T|T|T|T|T|T|T~~|T|T~~|T|T~|T~~|T~~|Tٓٓ|T~|T|T~??(  pUܛ|T|T*Fߞ}U|T#ښtZt|TUܛ|T|T*Uܛ|T|T*Бn|T}U|TFk/}T|T|T~#y*mi?b^i~~Ԑbdbb^i~Аԙihpwowor^i~Fƛipʉhki~8~ˌ^^rdq~p00tortoisehg-2.10/icons/menusynch.ico0000664000076400007640000001373612100577421016524 0ustar stevesteve  6 h  F( @ NNjQOOOPON.N_P]iqpld [PPNaN N Q U"u(}'{$y"wtn_RNNONQ`,ق-ڃ,ف*׀(}%zp RPN?NNNONNP^0܅2߇2߇0݆.ۃ+׀nPOUNNkPZ0܅46641ކgPN-NPN+P+~37::7kQNNQN7Qk/܄37;;+}QNNaO W*.ڃ1އ573݆ RNANnOr(}+؁.ۄ1ކ2ވ\PNok"w%z(}+׀-قoQNN NNNNNNNNNNNNNNmqt"w%z'|(|QN6N?NNOOOOOOOOOONNjnqs!v#xdPN.O \gggggggggNNgjmprrPNNNO\llkjigggNNggikmodON"NPappomkigNNggggijk_ONN"Ph tsrpmjNNgggggggg ZONNNP#x#x!vsqnNNggggggggg \ON.Qf)~'|%z"wtqNNOOOOOOOOOONNN?N6 R,ׁ-ق+׀(}%z"wsNNNNNNNNNNNNNNN NQ!r2߈1ކ.ۄ+؁(}o!uNP\7751އ.ڃ$wOsNNA R3݇;;73/܄ WOdNNP(y7::73nQN7 RNNQh1ކ4664-׀QN+PNN-Pd+׀.ۃ0݆2߇2߇/ۅZPNkNOUPj%z(}*׀,ف-ڃ-ق]PNNONNNN@OQjt"w$y'{(}(}_PNONNR \hmps!u"wp TPN N NaPP Y`gkle[ON_N.OPOOOPNjNxp0 ?? ?(  |(|(|(|(|(|(2<<2<|(|(|(HHH|(|(|(|($mH$m|(|(2<$m<|(|(<$m<|(|(|(|(|(|(|(|(|(<<<|(|(2222|(|(2<<|(|(<<2|(|(2222|(|(<<<|(|(|(|(|(|(|(|(|(<$m<|(|(<$m<2|(|($mH$m|(|(|(|(HHH|(|(|(<2<<2|(|(|(|(|(|(?`A(  0<<20|(00HH|(|(4_|(|($mH$m|(0|(<$m<|(5|($m<|(|(<<|(+|(|(|(|(|(|(|(|(|(|(|(|(+|(<<|(|(<$m|(5|(<$m<|(0|($mH$m|(|(4_|(|(HH00|(02<<0ptortoisehg-2.10/icons/scalable/0000755000076400007640000000000012235634575015566 5ustar stevestevetortoisehg-2.10/icons/scalable/apps/0000755000076400007640000000000012235634575016531 5ustar stevestevetortoisehg-2.10/icons/scalable/apps/system-file-manager.svg0000664000076400007640000001616312100577421023117 0ustar stevesteve image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-2.10/icons/scalable/apps/tools-spanner-hammer.svg0000644000076400007640000010756612110205645023324 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz preferences settings control panel tweaks system tortoisehg-2.10/icons/scalable/apps/help-browser.svg0000664000076400007640000003224012100577421021651 0ustar stevesteve image/svg+xml Help Browser 2005-11-06 Tuomas Kuosmanen help browser documentation docs man info Jakub Steiner, Andreas Nilsson http://tigert.com tortoisehg-2.10/icons/scalable/apps/utilities-terminal.svg0000664000076400007640000001312012100577421023060 0ustar stevesteve image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg C:>_ tortoisehg-2.10/icons/scalable/apps/help-readme.svg0000644000076400007640000007065012110205645021425 0ustar stevesteve image/svg+xml Generic Text text plaintext regular document Jakub Steiner http://jimmac.musichall.cz tortoisehg-2.10/icons/scalable/apps/thg-logo.svg0000664000076400007640000012272412100577421020767 0ustar stevesteve image/svg+xml TortoiseHg 2007-dec-11 Peer Sommerlund Closely resembles TortoiseSVN logo Hg tortoisehg-2.10/icons/scalable/apps/tools-hooks.svg0000644000076400007640000010337712145761533021541 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz preferences settings control panel tweaks system tortoisehg-2.10/icons/scalable/apps/preferences-desktop-font.svg0000664000076400007640000000763712100577421024170 0ustar stevesteve fonts.svg image/svg+xml fonts.svg 2010-06-26 Johan Samyn TortoiseHg project F f tortoisehg-2.10/icons/scalable/status/0000755000076400007640000000000012235634575017111 5ustar stevestevetortoisehg-2.10/icons/scalable/status/thg-warning.svg0000664000076400007640000001547112100577421022054 0ustar stevesteve Warning image/svg+xml Warning Yuki Kodama TortoiseHg Project 2010-05-31 tortoisehg-2.10/icons/scalable/status/thg-error.svg0000664000076400007640000001342612100577421021536 0ustar stevesteve Error image/svg+xml Error Yuki Kodama TortoiseHg Project 2010-05-29 tortoisehg-2.10/icons/scalable/status/thg-file-p1.svg0000664000076400007640000000634212100577421021641 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/status/thg-removed-subrepo.svg0000644000076400007640000002203612235634453023526 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-2.10/icons/scalable/status/thg-file-p0.svg0000664000076400007640000000633112100577421021636 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/status/hg-patch-applied.svg0000644000076400007640000004667412231647661022756 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/status/hg-patch-unguarded.svg0000644000076400007640000003146312231647661023304 0ustar stevesteve QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-2.10/icons/scalable/status/hg-patch-guarded.svg0000644000076400007640000003011412231647661022731 0ustar stevesteve QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-2.10/icons/scalable/status/thg-success.svg0000664000076400007640000001173712100577421022060 0ustar stevesteve Success image/svg+xml Success Yuki Kodama 2010-05-29 TortoiseHg Project tortoisehg-2.10/icons/scalable/status/thg-file-merged.svg0000664000076400007640000001015712100577421022563 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/status/thg-remote-repo.svg0000644000076400007640000024767612110205645022656 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz HTML hypertext web tortoisehg-2.10/icons/scalable/status/thg-svn-subrepo.svg0000644000076400007640000001524112110205645022660 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en tortoisehg-2.10/icons/scalable/status/thg-git-subrepo.svg0000644000076400007640000001112112110205645022626 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en tortoisehg-2.10/icons/scalable/status/thg-added-subrepo.svg0000644000076400007640000002100312235634453023117 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-2.10/icons/scalable/status/thg-subrepo.svg0000644000076400007640000001420312110205645022051 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-2.10/icons/scalable/actions/0000755000076400007640000000000012235634575017226 5ustar stevestevetortoisehg-2.10/icons/scalable/actions/hg-rebase.svg0000664000076400007640000007223112100577421021576 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/copy-hash.svg0000664000076400007640000004554612100577421021645 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" # # tortoisehg-2.10/icons/scalable/actions/qfinish.svg0000664000076400007640000004425512100577421021407 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/hg-init.svg0000664000076400007640000002272512100577421021303 0ustar stevesteve image/svg+xml Initialize 2008-12-27 Peer Sommerlund Icon for TortoiseHg dialog "Init" tortoisehg-2.10/icons/scalable/actions/view-annotate.svg0000664000076400007640000001140712100577421022520 0ustar stevesteve view annotations image/svg+xml view annotations 2011-02-16 Peer Sommerlund # tortoisehg-2.10/icons/scalable/actions/dialog-warning.svg0000664000076400007640000003352512100577421022646 0ustar stevesteve image/svg+xml Dialog Warning 2005-10-14 Andreas Nilsson Jakub Steiner, Garrett LeSage dialog warning tortoisehg-2.10/icons/scalable/actions/hg-unbundle.svg0000644000076400007640000004217112110205645022144 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-annotate.svg0000664000076400007640000002237312100577421022150 0ustar stevesteve Annotate Document image/svg+xml Annotate Document 2008-04-16 Peer Sommerlund Icon for TortoiseHg tab "Annotate" annotate blame image/svg+xml tortoisehg-2.10/icons/scalable/actions/hg-qpop.svg0000664000076400007640000001056712100577421021320 0ustar stevesteve QPop image/svg+xml QPop 2010-10-17 Peer Sommerlund QPop icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-incoming.svg0000664000076400007640000002710012100577421022133 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Incoming icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-archive.svg0000664000076400007640000003367612100577421021770 0ustar stevesteve image/svg+xml Media Floppy Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-pull.svg0000664000076400007640000004010312100577421021302 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-qpush-all.svg0000664000076400007640000001430712100577421022243 0ustar stevesteve QPushAll icon image/svg+xml QPushAll icon 2011-01-25 Patrice LACOUTURE QPushAll icon for TortoiseHg - derived from QPush by Peer Sommerlund. Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/view-file.svg0000664000076400007640000001101712100577421021623 0ustar stevesteve View changes in file image/svg+xml View changes in file 2011-02-16 Peer Sommerlund tortoisehg-2.10/icons/scalable/actions/go-previous.svg0000664000076400007640000001751312100577421022222 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Previous go previous left arrow pointer < tortoisehg-2.10/icons/scalable/actions/application-exit.svg0000664000076400007640000002343612100577421023216 0ustar stevesteve ]> tortoisehg-2.10/icons/scalable/actions/hg-compress.svg0000664000076400007640000005126012100577421022167 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz/ package archive tarball tar bzip gzip zip arj tar jar tortoisehg-2.10/icons/scalable/actions/go-up.svg0000664000076400007640000001777312100577421021002 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Up go higher up arrow pointer > Andreas Nilsson tortoisehg-2.10/icons/scalable/actions/hg-transplant.svg0000664000076400007640000000735312100577421022526 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/actions/hg-rename.svg0000644000076400007640000002160012135406414021575 0ustar stevesteve image/svg+xml 2010-06-06 Johan Samyn Icon for TortoiseHg dialog "Rename" TortoiseHg Project image/svg+xml tortoisehg-2.10/icons/scalable/actions/thg-qrefresh.svg0000644000076400007640000005405312110205645022335 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/hg-update.svg0000664000076400007640000007721512100577421021626 0ustar stevesteve Update image/svg+xml Update Yuki Kodama 2010-05-29 TortoiseHg Project tortoisehg-2.10/icons/scalable/actions/hg-status.svg0000664000076400007640000002514612100577421021663 0ustar stevesteve image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-2.10/icons/scalable/actions/copy-patch.svg0000664000076400007640000004062412100577421022011 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/edit-file.svg0000644000076400007640000002621512110205645021577 0ustar stevesteve Edit File image/svg+xml Edit File 2011-03-03 Peer Sommerlund Edit file icon for TortoiseHg Angel Ezquerra, Garrett LeSage, Jakub Steiner, Steven Garrity tortoisehg-2.10/icons/scalable/actions/edit-cut.svg0000664000076400007640000005501312100577421021456 0ustar stevesteve image/svg+xml Edit Cut Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-2.10/icons/scalable/actions/view-filter.svg0000664000076400007640000003661412100577421022203 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz tortoisehg-2.10/icons/scalable/actions/hg-remove.svg0000644000076400007640000001524712110205645021631 0ustar stevesteve image/svg+xml Remove 2008-05-07 Peer Sommerlund Remove icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/new-group.svg0000664000076400007640000002162512100577421021665 0ustar stevesteve image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/process-stop.svg0000664000076400007640000002750212100577421022403 0ustar stevesteve image/svg+xml Stop 2005-10-16 Andreas Nilsson stop halt error Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-purge.svg0000664000076400007640000003414512100577421021461 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/actions/view-diff.svg0000664000076400007640000001302412100577421021614 0ustar stevesteve view diff image/svg+xml view diff 2011-02-16 Peer Sommerlund tortoisehg-2.10/icons/scalable/actions/qimport.svg0000664000076400007640000004416312100577421021437 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/hg-qgoto.svg0000664000076400007640000003021312100577421021460 0ustar stevesteve QPushAll icon image/svg+xml QPushAll icon 2011-01-25 Patrice LACOUTURE QPushAll icon for TortoiseHg - derived from QPush by Peer Sommerlund. Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/thg-repository-open.svg0000664000076400007640000002116712100577421023701 0ustar stevesteve image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-commit.svg0000664000076400007640000000472212100577421021625 0ustar stevesteve image/svg+xml tortoisehg-2.10/icons/scalable/actions/thg-password.svg0000664000076400007640000004560212100577421022365 0ustar stevesteve tortoisehg-2.10/icons/scalable/actions/hg-bundle.svg0000644000076400007640000004516612135406414021614 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Push icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-right-chunks.svg0000664000076400007640000002452312100577421025360 0ustar stevesteve Chunk-to-right image/svg+xml Chunk-to-right Peer Sommerlund Lapo Calamandrei, Jakub Steiner 2011-01-09 tortoisehg-2.10/icons/scalable/actions/edit-copy.svg0000664000076400007640000003635312100577421021643 0ustar stevesteve image/svg+xml Edit Copy 2005-10-15 Andreas Nilsson edit copy Jakub Steiner tortoisehg-2.10/icons/scalable/actions/thg-shelve-delete-left.svg0000664000076400007640000000666112100577421024203 0ustar stevesteve Delete-files-left image/svg+xml Delete-files-left 2011-01-09 Peer Sommerlund tortoisehg-2.10/icons/scalable/actions/thg-add-subrepo.svg0000644000076400007640000002017712110205645022723 0ustar stevesteve unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-2.10/icons/scalable/actions/hg-qfold.svg0000664000076400007640000002645012100577421021444 0ustar stevesteve QPushAll icon image/svg+xml QPushAll icon 2011-01-25 Patrice LACOUTURE QPushAll icon for TortoiseHg - derived from QPush by Peer Sommerlund. Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/status-check.svg0000644000076400007640000003647112110205645022340 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/edit-find.svg0000664000076400007640000010512412100577421021602 0ustar stevesteve image/svg+xml Edit Find edit find locate search Steven Garrity Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-bisect-bad-good.svg0000664000076400007640000004232112100577421023255 0ustar stevesteve image/svg+xml Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-2.10/icons/scalable/actions/go-jump.svg0000664000076400007640000001766212100577421021326 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Jump go jump seek arrow pointer tortoisehg-2.10/icons/scalable/actions/view-hidden.svg0000644000076400007640000004760012135406414022145 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/thg-sync.svg0000664000076400007640000001446112100577421021476 0ustar stevesteve image/svg+xml Sync 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/settings_projrc.svg0000644000076400007640000004237612110205645023162 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-import.svg0000664000076400007640000004771312100577421021656 0ustar stevesteve image/svg+xml Media Floppy Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-2.10/icons/scalable/actions/thg-log-load-all.svg0000664000076400007640000003001312100577421022755 0ustar stevesteve loadall image/svg+xml loadall Johan Samyn TortoiseHg Project 2010-09-01 N 0 tortoisehg-2.10/icons/scalable/actions/hg-grep.svg0000664000076400007640000001316112100577421021267 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-left-chunks.svg0000664000076400007640000003035512100577421025175 0ustar stevesteve Chunk-to-left image/svg+xml Chunk-to-left Peer Sommerlund Lapo Calamandrei, Jakub Steiner 2011-01-09 tortoisehg-2.10/icons/scalable/actions/ldiff.svg0000664000076400007640000004312112100577421021021 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/hg-bisect.svg0000664000076400007640000006770512100577421021620 0ustar stevesteve image/svg+xml Edit Cut Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-2.10/icons/scalable/actions/thg-console.svg0000664000076400007640000010274212100577421022164 0ustar stevesteve showlog image/svg+xml showlog Johan Samyn TortoiseHg Project 2010-09-01 tortoisehg-2.10/icons/scalable/actions/hg-export.svg0000664000076400007640000004767612100577421021675 0ustar stevesteve image/svg+xml Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-right-all.svg0000664000076400007640000003452512100577421024640 0ustar stevesteve image/svg+xml Media Seek Forward Lapo Calamandrei Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-verify.svg0000664000076400007640000003774612100577421021655 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-left-all.svg0000664000076400007640000003364012100577421024452 0ustar stevesteve image/svg+xml Media Seek Backward Lapo Calamandrei tortoisehg-2.10/icons/scalable/actions/hg-qpop-all.svg0000664000076400007640000001625312100577421022064 0ustar stevesteve QPopAll image/svg+xml QPopAll 2011-01-25 Patrice LACOUTURE QPopAll icon for TortoiseHg - derived from QPop by Peer Sommerlund Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/thg-shelve-delete-right.svg0000664000076400007640000000655712100577421024372 0ustar stevesteve Delete-files-right image/svg+xml Delete-files-right 2011-01-09 Peer Sommerlund tortoisehg-2.10/icons/scalable/actions/view-at-revision.svg0000664000076400007640000005154212100577421023153 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/thg-mq.svg0000664000076400007640000001233512100577421021135 0ustar stevesteve MQ Icon image/svg+xml MQ Icon 2011-01-28 Patrice LACOUTURE MQ icon for TortoiseHg - derived from QPush icon by Peer Sommerlund. Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/hg-bisect-good-bad.svg0000664000076400007640000004560012100577421023260 0ustar stevesteve image/svg+xml Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-left-file.svg0000664000076400007640000002427212100577421024622 0ustar stevesteve image/svg+xml Media Playback Start Lapo Calamandrei play media music video player Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-qguard.svg0000664000076400007640000003162012100577421021615 0ustar stevesteve QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/compare-files.svg0000664000076400007640000004366712100577421022502 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" = ? tortoisehg-2.10/icons/scalable/actions/go-down.svg0000664000076400007640000002015412100577421021310 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Down go lower down arrow pointer > Andreas Nilsson tortoisehg-2.10/icons/scalable/actions/hg-extensions.svg0000664000076400007640000001275012100577421022534 0ustar stevesteve Extensions image/svg+xml Extensions 2010-11-13 Johan Samyn Extensions icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/go-next.svg0000664000076400007640000001731412100577421021323 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Next go next right arrow pointer > tortoisehg-2.10/icons/scalable/actions/thg-reporegistry.svg0000664000076400007640000012713212100577421023260 0ustar stevesteve repotree image/svg+xml repotree Johan Samyn TortoiseHg Project 2010-09-01 tortoisehg-2.10/icons/scalable/actions/hg-merge.svg0000664000076400007640000001300012100577421021421 0ustar stevesteve image/svg+xml Merge 2008-04-26 Peer Sommerlund tortoisehg-2.10/icons/scalable/actions/hg-recover.svg0000664000076400007640000003772512100577421022013 0ustar stevesteve image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/hg-log.svg0000644000076400007640000002226712110205645021115 0ustar stevesteve image/svg+xml Log 2008-05-19 Peer Sommerlund Log icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-revert.svg0000664000076400007640000001262112100577421021641 0ustar stevesteve image/svg+xml Revert 2008-05-18 Peer Sommerlund Revert icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-clone.svg0000664000076400007640000011736212100577421021442 0ustar stevesteve Clone image/svg+xml Clone Yuki Kodama TortoiseHg Project 2010-05-29 tortoisehg-2.10/icons/scalable/actions/visualdiff.svg0000664000076400007640000003043312100577421022073 0ustar stevesteve image/svg+xml Repo Browse 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-2.10/icons/scalable/actions/tasktab-refresh.svg0000664000076400007640000002300412100577421023020 0ustar stevesteve reloadttimage/svg+xmlreloadttJohan SamynTortoiseHg Project2010-08-23 T tortoisehg-2.10/icons/scalable/actions/hg-tag.svg0000664000076400007640000001155512100577421021112 0ustar stevesteve Tag image/svg+xml Tag 2010-05-29 Yuki Kodama TortoiseHg Project tortoisehg-2.10/icons/scalable/actions/hg-outgoing.svg0000664000076400007640000002671212100577421022173 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Outgoing icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/thg-shelve-move-right-file.svg0000664000076400007640000002564212100577421025007 0ustar stevesteve image/svg+xml Media Playback Start Lapo Calamandrei play media music video player Jakub Steiner tortoisehg-2.10/icons/scalable/actions/hg-undo.svg0000664000076400007640000001270312100577421021300 0ustar stevesteve image/svg+xml 2008-05-18 Peer Sommerlund Revert icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/view-refresh.svg0000664000076400007640000001560412100577421022350 0ustar stevesteve ]> tortoisehg-2.10/icons/scalable/actions/document-new.svg0000664000076400007640000004177612100577421022360 0ustar stevesteve image/svg+xml New Document Jakub Steiner http://jimmac.musichall.cz tortoisehg-2.10/icons/scalable/actions/hg-pull-to-here.svg0000664000076400007640000006374712100577421022666 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/hg-qpush.svg0000664000076400007640000001042612100577421021473 0ustar stevesteve QPush image/svg+xml QPush 2010-10-17 Peer Sommerlund QPush icon for TortoiseHg tortoisehg-2.10/icons/scalable/actions/go-to-rev.svg0000644000076400007640000003157412110205645021560 0ustar stevesteve image/svg+xml Jakub Steiner http://jimmac.musichall.cz go jump seek arrow pointer tortoisehg-2.10/icons/scalable/actions/hg-qdelete.svg0000664000076400007640000002325512100577421021762 0ustar stevesteve QPushAll icon image/svg+xml QPushAll icon 2011-01-25 Patrice LACOUTURE QPushAll icon for TortoiseHg - derived from QPush by Peer Sommerlund. Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/thg-qreorder.svg0000664000076400007640000001443712100577421022350 0ustar stevesteve QReorder icon image/svg+xml QReorder icon 2011-01-25 Patrice LACOUTURE QReorder icon for TortoiseHg - derived from QPush icon by Peer Sommerlund Peer Sommerlund, Patrice LACOUTURE tortoisehg-2.10/icons/scalable/actions/mail-forward.svg0000664000076400007640000011027112100577421022322 0ustar stevesteve image/svg+xml Mail Jakub Steiner Andreas Nilsson mail e-mail MUA tortoisehg-2.10/icons/scalable/actions/go-home.svg0000644000076400007640000005013112110205645021262 0ustar stevesteve image/svg+xmlGo HomeJakub Steinerhttp://jimmac.musichall.czhomereturngodefaultuserdirectoryTuomas Kuosmanen tortoisehg-2.10/icons/scalable/actions/hg-push.svg0000664000076400007640000004012212100577421021306 0ustar stevesteve image/svg+xml 2011-02-14 Peer Sommerlund Push icon for TortoiseHg tortoisehg-2.10/icons/branch.ico0000664000076400007640000001027612100577421015744 0ustar stevesteve  ( @ ͯ|ͯͯͯ|fFnOlNeFgHpRoQgGiIrTrThIjKuXtWoPoAkMx[wZvYqSoAmNz]z]x\x[rToAnP}`|`{_z^y\sUoApQcb~b}a|_z^tWoA qSgfec~b}`{_uWoA?gGbBrTjiz]y\ec~b|`uXoA?hIpQfFsUmkrTn?y]fdb}`uXoAjKsUqSgHƺtWpntVn?z^geb}avYz^PkLuXsUhIvXsqtWn?z^gec}asUkMwZtWiIŹ wZvtvXn?{^heb}`z^x\vYiJ x[xwwZn?{^geb|`z]x[jKz]{yx[@dC`?԰z^gd~b{_y\kL{^Ö~”|y\?y[pmkifc}`z^kMȼ |`Řė~z^?{^tromjge~b|_lM}aǛř{^hxvtqnligcx\aA~bȝǜ|`?|ͯͯͯžͯͯͯͯί dʟɞ~anMhhhqSe̢ʟ~bmNƯz^y]x[nMqSҪҪѩѨqSƵfͣˡcnPƚĘ•|y|rUԭӬҪѩÕ}Τ̢wɞǛŘÖ}lMrUծԭҪѨϦͤˡɞǜřmNsUծӬѩϦͤˡɞǜmOrUԭҪШΥ̢ʠnPrUҪЧΤ̢oQqSϦͤoQnnx0tortoisehg-2.10/icons/filedelete.ico0000664000076400007640000001246612100577421016614 0ustar stevesteve  & h( @ &&&&++++))&&0000..++))&&::552200..++))&&::77552200..++))&&::77552200..++))&&::77552200..++))&&$$!!::77552200..++))&&$$!!::77552200..++))&&$$!!::77552200..++))&&$$!!::77552200..++))&&$$<<::77552200..++))&&AA??<<::77552200..++))&&FFCCAA??<<::77552200..++))&&KKHHFFCCAA??<<::77552200..++))&&PPMMKKHHFFCCAA??<<::77552200..++))&&TTRRPPMMKKHHFFCCAA::77553300..++))&&VVUUTTRRPPMMKKHHFF::77553300..++))&&VVVVUUTTRRPPMMKK::77553300..++55VVVVUUTTRRPP::7755330055VVVVUUTT::775555VVVV::55??????(  @@@@@""++@@  @4400++@@@**4400++ @@**4400++&&!!@@**4400++&&@@//994400++@@66CC>>994400++@@==MMHHCC//**4400++@++UURRMM66@@**5500++@@@UU==@@**55&&@@++@@@AAǬAAAAAAAAAAAǬAAAtortoisehg-2.10/icons/settings_user.ico0000664000076400007640000001246612100577421017410 0ustar stevesteve  & h( @ DDDDDDDDDDDDDDDDDDDDDDDDDDNOOOOOOOODmDN [`______D%dODNdnkgggggDEzN,h)fDNhspmjgggDnN7oG{wĝDOk$y!vroligDf]_DOn)&{#x uqnkDվ:rDO!r/ۄ,؁)~%z"wtpDcʪDP%u41ކ.ڃ+׀(}$y!vDZDQ"q9630݅-ق*'|DDN]><952߈/܄,؁DDN#s>>;841އDϟrϟrϟrϟrϟrϟrϟrϟrϟrТvӨ֮ٴܺƫ̴θDO_%u.-Ԁ+})zDDDDDDDDDDDDDDDDDDDDPQNNNNNNNNNP~~3GM;~^nnnnj7}~knnnnnnn.Ywnnnnnnf~}wnnnnnn }}wnnnnn3}}vnnnn#~jvnnh}vn4~.E}Op{W%~~}?????(  ˛nַַַַַַַַ˛nַַN V W W WַWַNkkggַhSsַO!t"vpjַˬӺַO*}-ق&{ tַʪַO*|71ކ+׀ַַZ-3݇.؂ÇOǏ[Ǐ[Ǐ[Ǐ[Ȓ_˘hΞpѤyɓaPN qO@(UY4}@%unnn.RxnnK}GwnB~@¿jƿ€9>ĿAAAAAAAAAAA?A?AAAAtortoisehg-2.10/icons/menudelete.ico0000664000076400007640000001373612100577421016642 0ustar stevesteve  6 h  F( @ ErrE  ((**((%%,,..,,**((%%E002200..,,**((%% E##66442200..,,**((%%  --8866442200..,,**((%% r::8866442200..,,**((%%r::8866442200..,,**((%%##!!::8866442200..,,**((%%##!!::8866442200..,,**((%%##!! ::8866442200..,,**((%%##!! ::8866442200..,,**((%%##!!::8866442200..,,**((%%##<<::8866442200..,,**((%%!!AA??<<::8866442200..,,**((%%""EECCAA??<<::8866442200..,,**((%%$$IIGGEECCAA??<<::8866442200..,,**((%%%%MMKKIIGGEECCAA??<<::8866442200..,,**((%%''QQOOMMKKIIGGEECCAA??<<::8866442200..,,**((%%rVVSSQQOOMMKKIIGGEECCAA::8866442200..,,**((%% rBBVVVVSSQQOOMMKKIIGGEE!!::8866442200..,,**((55VVVVVVSSQQOOMMKKII##::8866442200..,,**E OOVVVVVVSSQQOOMM%%::8866442200..((E PPVVVVVVSSQQ''::88664422,, PPVVVVVV))::886600 ;;JJ""--##OrE???(  llllllHHlllllllllllll?(  llllllHH35(*&(l36ll01llllllllll0pp00tortoisehg-2.10/icons/thg_logo_92x50.png0000664000076400007640000002003312100577421017162 0ustar stevestevePNG  IHDR\2rsRGBbKGD pHYsHH1XItIME EMIDATxy]U;[cRCR** 0 A@6BVADFD峻D!@#2 CBU[w8gܪh8^uSy> ne-,~;L%&L4Wzן.3G4KN6'd~`?۟fVJp؉f|N]Cw<?| _Mo:X`20LZ4_vQֶ CK-0e2wdY it巀 +g?VE!fŴ:כX,fl h}=;nUY11߻{ܻ l*l* 7}3fUN5u Ǝ̷,G p@}Ӂǎ<8Ù1c'M۶18{tk֬rIկ_~2< W冼{vYx3iLeеm;\vXKYXGxs OFI}_.>3f̘ .K2sLCP!㢵Fk K06Nַ͖-[b˖{mvBoo6=~7.X,Sl5_HK]C=;w(ꢮq STlYg}XF)E $$~ DJ a1ڀ!XxQkkks= AP*|<"B!Zk˶b$ 4L\.K. ;oѫZ?ђ$cRs8 0eZw䘎iq2Mnʑ&ΘUok.,_7!JЁ+EBTьf{ضM<'Jfښj}ur)ɋ?䐜f9Oye1{~C,ۦKo.\X<7سNgʜ9<zkMMMgu]{xPHXT*Qv\\׍@V* QuG\k3%,l&LɤbT}G1gΡ<_sK_ 8(>q%cthl%?p<\P?o<_Sɽt ;wۚ_x3~$R((8W]{PC6h$:DJeYx}`[Dl6Qu41t*:իz5(2=]3}}+c] 7O_Lc7d](ȎΆ'5~o-+̻UN:|D"  W=:WQ,!g>^z T(IH}h'r@J<<@=l~ɥbqB!R!(+xp;D~C?N8g\p@S J8C):P_^yj9&E,aB WO.c ;{7,Xp#RX,c-L&MmmE ,-b19"+vuw󵫯w]R^q%;6mIJJiF9(J@׋Sv!,#*}9r@KXn l3]z饱 AJJ؞K *5ŲK(%Y `Ym² "E^zE8ma1!n}0 Ս BmC:T[8K0J<~=xsGM} ۧqL9 (a~vvX("}Iy`  m@6ц|O]b؇R~ <1vɸq:J q=RI>%䈞U H+K()U_C}m5E٦qVz-xI$̞w8R tuuHeD 4$6LgYNysqJ%r7_< 0AEo? €@ؼs3C!|G $,KXAH*T gСoW={+?SJ<5V-y5K]Y?vk%J.N١T.ء GDh+,HP[T:hlEmm]vNp(uNgB ZK X:FU6E.SNw=|a/Q[W(B"$2((ٻB0ⱡ bs „Hee;A9Xn;1מr3>az6o>7ccVW))S4s9H)q]rى@*E+4]/R')*KE )n*GщֆPE ;/l! IPK{vzz1>kWd)%"E4QZC!(J!`6 ZF!0{}!w-+ z}SOvʕ5ưNvzMV_ԯ?>p/ÊAX08GɀD"y*X,"O"pD*TںxRlZ$j84J{pL)ҘпKŰb qoo#F P>ͥ/ z*=2OUob֙c8#еH$T~qRDtitcp=F)m"^mQFH$I8~Rtώxկ*0)ش<$?nƶ1k!HIj\憃HŨ!pր,PJc`4{vƲJ`ee˴m62zJayQCؽf[)\7)۷0Kt0sǑRbg2\xeW_ӹd*lx2F(#13D~S/K2!w{s|Fj(Tw[uV&Dz'Oa>c6Tt.e<ЃZ',>EO&Ԇḕ~>; 1_eYXEkUP[n1UX$[2~AsxΝ8{>,<m×R>)ɛ eHi&b߱qcRX=۶#WIdT }. JCʅN*R1<|y-G;."1c na[n i?4-:ZCXn$UÙ|aY| x{:vmLiji7V Z/> w:T&LUFQ.Eӳg'b ˈv^fYc1ijTlmdΎ4mQhBCǰYdveaG \nR%k|ƊC,e &ԆlhMʋ/]x^=ږNg*^ث8xځJb*E S(~n ڡE, 1!Z+B]!as3WjV|6?rG.fQ'>q t <7);y]{]dͮ!5M<_p7g&t ~2=L[MN>BX"E*ĄH8ʦwW?n/>3H]9<r^ 5\X^|/<81ƶOOMtNfzueUT2c:0ưckּRٚng2+߱%iaoG"388mصosNe߮-x83orqfoy.J/o]cNbΑ'rctjx@92!($n_?oՕcm1TO'z10Dzc~L;W3|LX9GXqs'*E-R " {fqhvx9$߼XN\>yr ڟd JD_儁?lqFj3 6FɊȣ/c>Nܶ,`CwG+n,6$ʍZED ;s3:4Zl}Y^|ҹZ€2\Uř9LFYeɤV߼7Tn[UIENDB`tortoisehg-2.10/icons/filemodify.ico0000664000076400007640000001027612100577421016636 0ustar stevesteve  ( @ tortoisehg-2.10/icons/menuimport.ico0000664000076400007640000000344612100577421016707 0ustar stevesteve h&  (  @|(|(<|(2|(O??=:7?|(<|(T4aO(y2|((GM() @?06r D v7!tG0"sI76o(g Eks*jV-,\E(n K75,gS-,= ME7-fP--S_J/1-*gQ--a/n?"y?*6fR--9!Vb-&|93tR-,3/BDK79 image/svg+xml tortoisehg-2.10/icons/TortoiseMerge.ico0000664000076400007640000005372612100577421017306 0ustar stevestevehf 00v h  !00 %.2( >:7B=;CBBNF@TKEWMG|_Rzrm{zy/Fi]iUnSqcwj|j}{~x}j~mwayf{hoHvJ|UU`v|rtsxW[̀g݀`ωr~ƛgnbelkosprv|x~~ʘҙћҝšĨ䤎ᤐᮝĩƱĴѺѽṫỮὰ¯ĺıƴɷʹν< KBLYN?:# Ta]XF"))* ^kg`YXNO ;SlgaagaHE'-%Skgqa`1E-&SusgX9$.E>Qtk]M96 SmZQlaN0 /5(\omlTOs`9991(doommTDHJH955dooml=BMH99'fomSBJ9,f\@2O( @- #!))),,,333410642444876?95:::<:8>=<F* S0%F>9FCBTFAQJE]RJ[TOUUUnPF`ZVf\Uh_Yl`Yld^saVqaXyi_kfcrhcth`wleuokunlspovpmvvvxtryuzzz|z~~~1 ')9L:P=P<^>S'Y6R$X!d:c1j9n?VFTBZLYH\MfYh_fReRcDeKfIlZkdmalan`pdpdtktlxm{oveqdyfth|k}m{j~mwpxp}rzt~w|x~z~|scbBhEmQoPpUx_nBoBpHqBtJvIxOzNxQzR~W|QX{uzvuzyywwyy{|~V[]_X^ƃlƆqƈsƊvƛb`aeegiklkmmpqtvy{~ƕǛǜȜƣƤŨ짐ƪŭůƱƷƾðıƴȶʸ̺νc--,#$-$))C͑aT) UU{ˑ*T  M͐-YWY"{e- U ,RK'$\K",FDza aBF[~~MHpf",2kNMPJitrn>͘Bvvrn@w4xvv5ō=B5ʤq<3Iġwq;BhȤmĞ.Rȡ6/7jRʤ80gj]l1:jR͖9Ĥh]sĤĤhR͞ĤyRĤĤ4ĽVy濷Ĥ?ĽĤXyзĤ?ĽUyҷ?00?????(0` $$$)))-,+---1/-50-111555999===u@:6@;8B>;xM>AAAKFBIIILLLTIASJEUHDQPOXQM\RORRRVVVXSPZZZ^^^yTGaWQfUPb\Xg\Xq^Q}^Te`^oaXjb]oc\ydVrcYtg_zh]aaafffmfbhhhlllvi`ukezhbnczmdrlhvni{qk~qi~tnpppuuu~vrzwvzzz~~~'+9U=Z:_?Vd3h7n>RA^L_Bf[cKeQp\nbreqcvhyoxfshwlzk}j{p|pv~y~q~qdCgJlNqGtJwNvJyTzR~V|Qvvu~vwv{zqv|zy}~[^[~™Ǜccfehkjknlpquuz{~šěœŜĝȜʟˢŲɷͼBBEEBBgbEE1Þ6@E7,EEEx] 6y|9@ Z]6  ^''  G va)%\ BE 1,~7;@  @DB6B5/2a6~S9#D#Rdu(*V;y{@>>#Yv3TmlK@;ĞW&1\gg\'$jmnmi 7#sX_UU`kprnnnmL¨orrnnmLf7ǹһsorrnrnmʼ҇sQrnrͿҾsQPrroQPsQPOoQOOsQPNOҷOONNҷOIoQIIIsHHHJJHJJJɼNNNҽsQNɾssŸ |?<?????(  @8888KrrrwwwKzlcxwvusrgƿTᣎƑvj555NE@TLF>;8ea^T⮝zb_]C?=|iwrXMFwixvtyTṬʸ¯䤏rduWK~ǛǛpbi]ssic8úǵūiU~l}kmRoI~x888Ű̻ǵʹũ~_bZ{SiR5ŭ˺n~TĨ`crXŭXfĨkWƴð{/n[ŲὲƯʸkFnu}e888ƵάūıwJz}xo{c8888Ữšxra8TƐ~~yfźTTŭƎ|nŻTTųƊvsŻT88888888( @ 4OOOJJJyyy< GED{rm~~~uuuMMM9=< qqq8888q+`[WˇIIImMMMztibyuokzzzWTRkfdhhhiiip8fYƈs8|nf{o[TOKKK ?95vewfQKGFCBf\U`ZVNNN8riƊvq YQLĹ~xm777222410876<:9444,,,#!XNH描A8laƇr(===---333RJD}m{yfyi_|k]RJ642k^V`ZV^^^A8woȶİsc_[Xh_Y>=9saVy|pdre]a^[8ztξʸıð짐ƅpF* eRyǛǛǛǛǛl`YnPFh_zu}rkfcqqq8ztνɷð]MfRziuzyvl`dJtJbBrhc~~~˺ƳŲıL:x_n`pdpdlZoPxQxOqIVFlll888qŮͽʸİe\Mkib_[X{SyPpHZLzzz:ƫϿ̺ƴξıı|QP=mfb][~W{S^>~yqƫͽXn?\Mmid_[cDqŨϿϿƳzavIj9P=mgbfI{qƤʹrVqBc1\MmmQqƷν®{^loBS0$}TGBƾƴkŲ`1 - T1&}qƩxpƴɷtnvS''Y6sqVqŮunƲ̻ı}kR$)hEzxrqU}qưwpƱϾɷnB9d:{ysopUqƱulư^X!X}zvpnnR}888qƴumŭzN^{zwpplnR888qtkƩqxzwsqnlqqq8|tlƕzwtqpP>>~yoRRR...IIIIIIvȝɞ{???IIIukeriVVVLLLppp]]]reƜvYYY===)))5551/.@;8@;72/-111---TIAxfLLL)))>YRN}ze`]$$$88821150-333:::<<<>>>333666(((A:5QQQ WWW+++111666,,,KFBuȝzkq^QMMMaWQvhXQM)))111CCC>>>VVVŲŲ333KOOO444///111000rcYȜȜȜȝěƜɝy***SJE~jb]___KKKY))),ͼͼŲŲqiiinc~qi===XSPzȝȜǛȜǛǛǛȜȜ›B>;555|ȝœsk~vr^^^,,,%ͼɷɷŲŲcccIIIppp!!!ikkkmfbɞȜǛǛȜǛǛǛȜǛȝydVzh]ɞȝɝ{kKKKCCCWWWVVVͼͼɷŲv^Lvi`ɟǛǛȜǛǛǛȜǛȜ-,+]]]~tnšʟˢ~ZZZͼɷŲŲŲkRA{pŜȜȜǛǛǛȜǛǝvg\X}^TcKzhbwvwojvnirlh___ͼɷŲoxM>ma™ƜȜȜȜȜŜvZZZ_BtJqGU={qkzmdͼͼɷŲŲoeQfUPoc\vh~q~qqcea_yTGgJwNwNtJdC\ROlll@@@aaaͼɷŲͼɷp|Qocp\shg[f[wllNyT~VzR{SwNtJZ:UHD>>>{ͼͼɷŲͼk[vJnkkgc^_[[~VzR{SwNtJ_?~ylll|||ͼɷŲŲɷ|QvJokkgc^_[[~VzR{SwNtKͼͼɷŲ[|Qn>nogfc^^[[~VzR{SͼŲŲ[|Qn>h7noghg^_[[~Vͼͼͼɷzk[vJn>h7nokfg^_[ͼͼŲp[|Qn>h7d3nojec^ͼͼɷzk[vJn>d3d3nnkfɷŲp[|Qn>h7Vd3nnͼŲze[d3d3ͼɷzp[VVɷŲued3u+uunͼŲzp[vJn>+++zuqnͼɷue|Q'''zz{urlɷŲuk99'zzuqrnͼɷ999zz{qqlnVVVz{uprni|Qn>Vzzzuqqlnie|Q|Qzzzvqprlliɷzk[zzuuqrmnlzpz{vuqqrllzzvwqqrlzz{{vqqrz{vvuq{{{ww{{{{{|{ ?|?<? ????tortoisehg-2.10/icons/menurepobrowse.ico0000664000076400007640000001373612100577421017567 0ustar stevesteve  6 h  F( @ 64.R53.54/64.(64.[53.+3=1?G66164.(64.Z53.*4?-FY3YpD^l88364.(4cΘ ͻ ̠ l964.Y53.*4@.GZ3ZqMuid{;<8DNH64.Y53.*5@.G[3ZrNvjs;;7 ˓CwPOOL: ;64.X53.+6A.H\3[sOwkz;<8750J ͘FwQOOL: 64.W53.+6B.I\4\tPxlz;<875/O ͘FvROOL: 64.V53.+6B.I]4]uQymz;<875/P ͘FuSOOL: 64.V53.+7C/J^5]uRznz;<875/Q ͘FtTOOL: 64.M53.+7D/K_6^vS{oz<=975/Q ͘FsUOOK9"n{75.t73/A33353.-8B/K`7_wT|pz<=975/R ͘FrVM@ji782@LCLdPU[>AA275.:9/5402BK8`xU}p{<=975/S ͘FqK;E@[yҺ~ԴyrwwֲCli796/661Kerq{<=975/T ͘Fp?OJh÷tvxskoѸID?A>0984gt<=975/T ͘Fo@E?dlmoqkdf˻UuMD?97/<=9;;775/M ̚+0155->74.-[4zŘhۭݙϏ ̀2vCjT\i75.;;' 76/A?6ժ@Ѓިn*Ѣ ђ̂-w>nOa_?;8/99+75/YYFBے@hnRܧՕ ЎЈ+{=286/64.76/333771*55-`64.c72,.UUU???(  T|EEERRRT|i?]<;;+RRRT|~l?]0MMT |RRRT|~lT|?}}C)EEET|~T|;___999999___*>T|;xxxt7'ڐT|?]6RRRpt7'___0[t7#ڐ999LLLڐ999___xxxڐ999lllڐlllxxxxxxlll999999lll?(  D=xZc/GSZRvKEF?Kw#WqvMoKƠCD,0K/S}%Vn}7/jBhl?/J}oUz)VkzC7z[I~qEks6F\fd7MWqZ9}^H|tBHGKLPt'c~d8UqCOƻgfd^?8"T_aEoSL=\}?i2JcjͼϣF?0p?w3ɬ̔wpaɮŴm]\Z++)fc[{0ptortoisehg-2.10/icons/22x22/0000755000076400007640000000000012235634575014577 5ustar stevestevetortoisehg-2.10/icons/22x22/actions/0000755000076400007640000000000012235634575016237 5ustar stevestevetortoisehg-2.10/icons/22x22/actions/window-close.png0000664000076400007640000000200712100577421021343 0ustar stevestevePNG  IHDRĴl;sRGBbKGD pHYs  tIME PIDAT8˝oU?gfޯhВH !jn Qv*K+\101jƤ,Rk"x?73o?^I̝ܹ{~gF."::ɦrⰧP&I'i4z|1@@mjj4n"+ @~hl%7 n q\T@B+D 4OL& *H͡2g,P\؀k&e3 8D␬ x4 hx}!eYzBM ^_ug8Jm1 J@2zL} $ py_' Ĩgq{p2g!Hb{ME`胻vzxTO"] $ ^h q}rEXL~N> l8DŅ^ yث?s-V!7/?CW5GUh_8"ǡgpѥi̥kl`V@ԀjGAݸ%^{ ,4ENGG}T C*o 7< r5(4'S3tw;9UПm8o~lY[.Iz(׉3INlޚ@o^d$r zZ١bC2y4[ ,׾DۅK"#*Kh$m`Γ,&61yf z @-pIJ|HVK& U).6'ƺ:Pm˿-6'窔@vp$0aDy~X٨- t=0@T*7QBے'2IENDB`tortoisehg-2.10/icons/16x16/0000755000076400007640000000000012235634575014605 5ustar stevestevetortoisehg-2.10/icons/16x16/apps/0000755000076400007640000000000012235634575015550 5ustar stevestevetortoisehg-2.10/icons/16x16/apps/kiln.png0000644000076400007640000000136012110205645017173 0ustar stevestevePNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb?cC_;oEmk]/̋ef5w/ /N2}8`g)LZ d԰gt{V\%KOQ`q%t5ǰ@9$y E?MMNO*%`xǗwl&:es;O해(ܶn˭wXY8XaX` FF2j bj ߿3vqֵ~'$}Wإ۟_|׌O0;a+* ^1y0(pK=az2¢ELh3+5~/uŮ:@Waև=Q*o/v>!!9u>qy׷σ%+%2s 6d8:t }܎&g>e`rgIFMDA(5L x>c b+qs1ʙɰ* zy0bAժEp<A#\鷉4;3z~L,XL9X52MN>?M^JqͯBse8IENDB`tortoisehg-2.10/icons/16x16/apps/gnupg.png0000644000076400007640000000127412170335562017372 0ustar stevestevePNG  IHDRasBIT|d pHYsu85tEXtSoftwarewww.inkscape.org<9IDAT8OHq?wk:e*QB2-,0tT (VQtT(:uЃѱKPt 2A $IPٖ6}O5ݴ=;=S"Ba[f*aGw$͸$~A[O޺R5gja:rG" Pߎw=%E4@=: @Kǫc=Whڝn_H.rE[[/&wtpr5'Wc 7!RSͫ9s %ˑ+؞ E!l: =P-G{6*ՃWAZogU$/ȹŦb`NUF,n yh,XYy^KKt 줜[[Hq-A 6F|g)h߹{ߦƗSK$3{QWqkSA6GSB~BLm,ѴÏE %"|@Fr%y t4SK\|3K2_0-XLv8qmdK]p7hv9IENDB`tortoisehg-2.10/icons/16x16/apps/reviewboard.png0000664000076400007640000000037412100577421020560 0ustar stevestevePNG  IHDRaIDAT8cd@?C0La>=( R 2H| l\6ETr H^,0?'ҋئ^cX x3e`A/R(0H. h6G (2n'[2bpR2{{R e&3r!(IENDB`tortoisehg-2.10/icons/ignore.ico0000664000076400007640000001246612100577421015775 0ustar stevesteve h&  (  T.   T,5 Z+,**NY; (W)6)' %j'_ <!xl$ufwpL ?}h?( @ ("x-(***)" 8.% -****** =EHq160+**** ~L FMRTD F*]970****f0 OW]bb> 1OL * L`i cb G~W/EN.WoDV' $/6 ;C2}Onq p``Otortoisehg-2.10/icons/menuadd.ico0000664000076400007640000001373612100577421016130 0ustar stevesteve  6 h  F( @ J J J J J J J J J J J J J J J J ČXĉT†MFJ J J J Ȓ_Ȑ[ŋU†MJ J J J ʖeʔaȐ[ĉTJ J J J J J J J ̙k̘gʔaƎYJ J J J J J J J J J J J J ͞rΜm̘gȒ_J J J J J J J J J سز֮ӪѦ~ҥzСtΜm̘gȒ_ƎYĉT†MFJ J J J ܺݹ۵ٱ֭ԩҥzСtΜm̘gʔaȐ[ŋU†MJ J J J ޾߽ݹ۵ٱ֭ԩҥzСtΜm̘gʔaȐ[ĉTJ J J J ܼ޾ܺڶزٱ֭ԩҥz͞r̙kʖeȒ_ČXJ J J J J J J J J زٱ֭Ѧ~J J J J J J J J J J J J J ڶ۵ٱӪJ J J J J J J J ܺݹ۵֮J J J J ޾߽ݹزJ J J J ܼ޾ܺسJ J J J J J J J J J J J J J J J (  ii|T|Tiٓi|T|T|T|THi|T|T|TiHH~iii|TiڐH$~ٓٓ|TiiiHٓ|T|T|TiHH|Ti|T|T|T????(  ii|T|Tiٓi|T|T|T|THi|T|T|TiHH~iii|TiڐH$~ٓٓ|TiiiHٓ|T|T|TiHH|Ti|T|T|T0000tortoisehg-2.10/icons/menucreaterepos.ico0000664000076400007640000001373612100577421017714 0ustar stevesteve  6 h  F( @ <lƓɽ ȽƓl<, 6gz^J@5+ ,,WcOOOOMB#PWdOOOOMB7PVeOOOOMB7PUeOOOOMB7PUfOOOOMB7PTgOOOOMB7PSgOOOOMB7PShOOOOMB7PRhOOOOMB7PRiOOOONB7PQiOOOONB7PQjOOOONC8PPkOOOONC8PPkOOOOMA5& PPlOOOMH6/ LW?4*  PPlOOOB;31,"`wH]+PARXF"*5@PD5"`{?) 6J]ppP804E-M`(Ds^Vrl1R^,Dv_VYE-M^($ 3I]ppS9*%5eD5"`~?H 0_Ƈ ɵ  ʶ{ԟkj,1,"`~HX+" *2?PLbPhLb?N*5  &,*.&-  ??(  O??=:7?T4aO(y2=4aO(y'f=Boa<1=Ap^<1<=pZ<1<;pW<2}<;pX<2}=;pY<$k=;pZ<jm=5eM%AI7v@;-D>1.E=,Maalj7aO254"v??? B??m(  O??=:7?0E/L4aO(y.5=3[yN.-x6ɿ=Ap^<18Ϳ<vi.r912j=,Maalj7aOǪ2ª7Ĵ8(?6h?97b?Ujlp00000tortoisehg-2.10/icons/menushowchanged.ico0000664000076400007640000001373612100577421017672 0ustar stevesteve  6 h  F( @ e@xಏxಏxಏxಏxಏxಏxe@kIkI44/673/A@@@kIuu( ˏ99/77.6:165.66/68078093--kITOOO4 ;;' 5915ZB5h4Ԃ4؅5k6gJ680880 kIUOOO?64.^5A44ۆ444445U?53.kIVOOO?64.{6W@4444445uQ670kIWOOO@66,465.5m44444w69163-ZkIuoa=XOOO@55.t64.6E85}U5Y6L;64.67/kIwYOOO@99/53.i6:16;264.u66/&e@xಏxಏxಏxuZu|ZOOO@P{[OOO@Pz\OOO@64.67/67067067067067066/64.ZPy\OOO@64.6L;5j5j5j5j5j6>464.`PxZOOO@64.v6O<444446D764.`8.>83+&"(64.>64.444446\C67/Jh6906T?4م4445k67/64.'s6x}Sͼ64.6806_E4݇4445jK68064.42Ζν!ͤ W64.#6805kK44445uP66/64.564..6705{T44445sO67064.&64.67/67/67/67/67/64.m64.<67/5Z4444߉5dG68064.6;16WA5zS5zS5zS5bF6:164.D64.4w4444؅68064.U6705yS4444z64.64.#6915uP44445cG64.64.6]D44446E767/6:15[44445yR66/64.d6B644444v6806:164.6;264.4y44445dG64.64.:64.4z44444{5\5gI5^4u44444߉6:164.V6806\C44444444444445j64.64.64."66/5g444444444444u68065.~65.y65.5tP4΀44444444چ5_6;266/64.?6916705nM5Z5f5r5s5d5}U6>468064.t64.64.64.W65.6:16:165.65.6:169164.r64. 64.64.???````p|@?@?`?``p?(  <;;+0MMT |?}}C);A);A)6ppt7'0[[t7#p[Np[Np[Np[Np[Np[Np[NLLLLcahcah|T|T|T|TKnuKnuBvBv|T|T|T|Tc2g5j8v@%{D)~H-K/GޒIץZΒK/c2g5j8u@%{D)~H-K/(  SGȪ>>1FZ\?BZ]@"f?M}[.h?LZ-j?GtuR+}gW{fWxhYvj\uk_z/wE^fO'r_Tqarerem`v}u~m2}r:RRPY+|}mUo4}p6xcmkfqpce{AeYOcgkix{qpAXm[mxAK}@Ñ^ӣr|oXo7xD"M-V6Z=kRpptortoisehg-2.10/icons/32x32/0000755000076400007640000000000012235634575014601 5ustar stevestevetortoisehg-2.10/icons/32x32/actions/0000755000076400007640000000000012235634575016241 5ustar stevestevetortoisehg-2.10/icons/32x32/actions/hg-sign.png0000644000076400007640000000255512170335562020302 0ustar stevestevePNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATXŗ_LW?B%fiFBPgt[lF%[tX4%`6E:%("* ,QiC@|r=|{5xf{耽{{ ֺߏ7[m|>_y@M6w9u\<3{Aaa$`qVZ}3ܯNj MHu:)[YY͛4n<O`uiN'z{c3T}Lzzzhnl>?|xx<-ANM쌂&91/cҌ1qB ݄ ,:WxW_9ڒ%,8GxY`MRX Dބ_f݃&PlcL#馤VI-%I+鑤^I:s!x?^tMk-???[5I7$ nY F0Ɣccf)@1][DŸkkSrƦݻv.b \tfm)$|4M.Ijk8cLtp43wJR1g/'MKҝ(ׁ%](rt :}uR$1 B`G;@PMRu$=dݡ'O+rDYE6ת{1vKBX4Aݱc7Ltʛl%Jʁ|W#)tgo$u*HδVJ 8 q sclnWEO$&)4>p}}*aXES]]}lC LSvvvV;|>ŋ[G[@cI79{qrOj-_Y7 4 K>y8a#⭕G}Ysgeď>(t3X$-G%ܼfO0\ZZ:Py a.;@&H@6o|45;u;gޑ}n,cy?% UDtvv\z]Jp<|&h٨^NN k;[v<+8׬9}Vm2: '&&'%yyDxvGe8tnO얤 E`wko4TPI)@Ef(˃@ "yv ޘIENDB`tortoisehg-2.10/icons/32x32/actions/hg-bookmarks.png0000664000076400007640000000265612100577421021331 0ustar stevestevePNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<@IDATxb?9W/2P-W`p_9 D < LҢ 1SDrbRٽ@Z Eq@ @R;QyPOnI!Q>:X 7/ Hu@@/d ] ̦ DjcP ̗9"c!wIY,ÿ Hu@7Вh(gDIV+pv7 \z _J' 0|̜@0 A)  @ B l< yE5&C KQ0gMek2Xݏ|<*/@az 6 (Ƞ[,~#H܆ @K@ŘW0xٜ2 Hs \+g^n]##!@l8=[ ?G~ OaFõa/OaL|& `H8[3 óg^ y!0' + o?0c('|_b:7\bft ,nCZ|ĂLlZçbfU0f:'p4u@tgcV| # @5?qr o,y 6վ|`' A!T20I߾CByZ4_?bHy %!;5!rE1#/7`п ׁb .0:]TDARp4Z] *^~G`_(?Grg|^fҰN(`q#Rrd`34iPQ7+#}Cjbb`@p=67C-IENDB`tortoisehg-2.10/icons/expander-close.png0000664000076400007640000000023712100577421017426 0ustar stevestevePNG  IHDR w&fIDATxcd 0'fxeπ)SԩSSd Ο>mb}r S/TK,EUSuWL|8 \A &IENDB`tortoisehg-2.10/icons/expander-open.png0000664000076400007640000000027412100577421017263 0ustar stevestevePNG  IHDR w&IDATxcd 0'fxeπ)SԩS*u+c ̬,{9ŝpi0& 11p6m GVWK,EUtgص_nn]1L,zJ ,}`OIENDB`tortoisehg-2.10/thg0000755000076400007640000001060612231647661013414 0ustar stevesteve#!/usr/bin/env python # # thg - front-end script for TortoiseHg dialogs # # Copyright (C) 2008-2011 Steve Borho # Copyright (C) 2008 TK Soh # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import sys argv = sys.argv[1:] if 'THG_OSX_APP' in os.environ: # Remove the -psn argument supplied by launchd if argv[0].startswith('-psn'): argv = argv[1:] # sys.path as created by py2app doesn't work quite right with demandimport # Add the explicit path where PyQt4 and other libs are bundlepath = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.join(bundlepath, 'lib/python2.6/lib-dynload')) if hasattr(sys, "frozen"): if sys.frozen == 'windows_exe': # sys.stdin is invalid, should be None. Fixes svn, git subrepos sys.stdin = None if 'THGDEBUG' in os.environ: import win32traceutil print 'starting' # os.Popen() needs this, and Mercurial still uses os.Popen if 'COMSPEC' not in os.environ: comspec = os.path.join(os.environ.get('SystemRoot', r'C:\Windows'), 'system32', 'cmd.exe') os.environ['COMSPEC'] = comspec else: thgpath = os.path.dirname(os.path.realpath(__file__)) testpath = os.path.join(thgpath, 'tortoisehg') if os.path.isdir(testpath) and thgpath not in sys.path: sys.path.insert(0, thgpath) # compile .ui and .qrc for in-place use fpath = os.path.realpath(__file__) if os.path.exists(os.path.join(os.path.dirname(fpath), 'setup.py')): from distutils.dist import Distribution from setup import build_qt build_qt(Distribution()).run() if 'HGPATH' in os.environ: hgpath = os.environ['HGPATH'] testpath = os.path.join(hgpath, 'mercurial') if os.path.isdir(testpath) and hgpath not in sys.path: sys.path.insert(0, hgpath) # Make sure to load threading by main thread; otherwise, _MainThread instance # may have wrong thread id and results KeyError at exit. import threading from mercurial import demandimport demandimport.ignore.append('win32com.shell') demandimport.ignore.append('tortoisehg.util.config') demandimport.ignore.append('icons_rc') demandimport.ignore.append('translations_rc') demandimport.enable() # Verify we can reach TortoiseHg sources first try: import tortoisehg.hgqt.run except ImportError, e: sys.stderr.write(str(e)+'\n') sys.stderr.write("abort: couldn't find tortoisehg libraries in [%s]\n" % os.pathsep.join(sys.path)) sys.stderr.write("(check your install and PYTHONPATH)\n") sys.exit(-1) # Verify we have an acceptable version of Mercurial from tortoisehg.util.hgversion import hgversion, checkhgversion errmsg = checkhgversion(hgversion) if errmsg: from mercurial import ui from tortoisehg.hgqt.bugreport import run from tortoisehg.hgqt.run import qtrun opts = {} opts['cmd'] = ' '.join(argv) opts['error'] = '\n' + errmsg + '\n' opts['nofork'] = True qtrun(run, ui.ui(), **opts) sys.exit(1) if ('THGDEBUG' in os.environ or '--profile' in sys.argv or getattr(sys, 'frozen', None) != 'windows_exe'): sys.exit(tortoisehg.hgqt.run.dispatch(argv)) else: import cStringIO mystderr = cStringIO.StringIO() origstderr = sys.stderr sys.stderr = mystderr sys.__stdout__ = sys.stdout sys.__stderr__ = sys.stderr ret = 0 try: ret = tortoisehg.hgqt.run.dispatch(argv) sys.stderr = origstderr stderrout = mystderr.getvalue() errors = ('Traceback', 'TypeError', 'NameError', 'AttributeError', 'NotImplementedError') for l in stderrout.splitlines(): if l.startswith(errors): from mercurial import ui from tortoisehg.hgqt.bugreport import run from tortoisehg.hgqt.run import qtrun opts = {} opts['cmd'] = ' '.join(argv) opts['error'] = 'Recoverable error (stderr):\n' + stderrout opts['nofork'] = True qtrun(run, ui.ui(), **opts) break sys.exit(ret) except KeyboardInterrupt: sys.exit(-1) except SystemExit: raise except: sys.__stderr__ = sys.stderr = origstderr raise tortoisehg-2.10/tortoisehg/0000755000076400007640000000000012235634575015074 5ustar stevestevetortoisehg-2.10/tortoisehg/__init__.py0000664000076400007640000000001512100577421017166 0ustar stevesteve#placeholder tortoisehg-2.10/tortoisehg/hgqt/0000755000076400007640000000000012235634575016037 5ustar stevestevetortoisehg-2.10/tortoisehg/hgqt/manifestdialog.py0000644000076400007640000004104612231647662021401 0ustar stevesteve# manifestdialog.py - Dialog and widget for TortoiseHg manifest view # # Copyright (C) 2003-2010 LOGILAB S.A. # Copyright (C) 2010 Yuya Nishihara # # 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. from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, fileview, status, filectxactions from tortoisehg.hgqt import revpanel from tortoisehg.hgqt.manifestmodel import ManifestModel class ManifestDialog(QMainWindow): """ Qt4 dialog to display all files of a repo at a given revision """ finished = pyqtSignal(int) linkActivated = pyqtSignal(QString) def __init__(self, repoagent, rev=None, parent=None): QMainWindow.__init__(self, parent) self._repoagent = repoagent self.setWindowIcon(qtlib.geticon('hg-annotate')) self.resize(400, 300) self._manifest_widget = ManifestWidget(repoagent, rev) self._manifest_widget.revChanged.connect(self._updatewindowtitle) self._manifest_widget.pathChanged.connect(self._updatewindowtitle) self._manifest_widget.grepRequested.connect(self._openSearchWidget) self._manifest_widget.setContentsMargins(10, 10, 10, 10) self.setCentralWidget(self._manifest_widget) self.addToolBar(self._manifest_widget.toolbar) self.setStatusBar(QStatusBar()) self._manifest_widget.showMessage.connect(self.statusBar().showMessage) self._manifest_widget.linkActivated.connect(self._linkHandler) self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth: dlgmeth(self), parent=self) self._readsettings() self._updatewindowtitle() @pyqtSlot() def _updatewindowtitle(self): self.setWindowTitle(_('Manifest %s@%s') % ( self._manifest_widget.path, self._manifest_widget.rev)) def closeEvent(self, event): self._writesettings() super(ManifestDialog, self).closeEvent(event) self.finished.emit(0) # mimic QDialog exit def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('manifest/geom').toByteArray()) self._manifest_widget.loadSettings(s, 'manifest') def _writesettings(self): s = QSettings() s.setValue('manifest/geom', self.saveGeometry()) self._manifest_widget.saveSettings(s, 'manifest') def setRev(self, rev): self._manifest_widget.setRev(rev) def setSource(self, path, rev, line=None): self._manifest_widget.setSource(path, rev, line) def setSearchPattern(self, text): """Set search pattern [unicode]""" self._manifest_widget._fileview.searchbar.setPattern(text) def setSearchCaseInsensitive(self, ignorecase): """Set if search is case insensitive""" self._manifest_widget._fileview.searchbar.setCaseInsensitive(ignorecase) def setFileViewMode(self, mode): self._manifest_widget.setFileViewMode(mode) @pyqtSlot(unicode, dict) def _openSearchWidget(self, pattern, opts): dlg = self._dialogs.open(ManifestDialog._createSearchDialog) opts = dict((str(k), str(v)) for k, v in opts.iteritems()) dlg.setSearch(pattern, **opts) dlg.runSearch() def _createSearchDialog(self): from tortoisehg.hgqt import grep return grep.SearchDialog(self._repoagent, []) @pyqtSlot(QString) def _linkHandler(self, link): ulink = unicode(link) if ulink.startswith('cset:'): repo = self._repoagent.rawRepo() changeid = hglib.fromunicode(ulink[len('cset:'):]) rev = repo[changeid].rev() self._manifest_widget.setRev(rev) else: self.linkActivated.emit(link) class ManifestWidget(QWidget, qtlib.TaskWidget): """Display file tree and contents at the specified revision""" revChanged = pyqtSignal(object) """Emitted (rev) when the current revision changed""" pathChanged = pyqtSignal(unicode) """Emitted (path) when the current file path changed""" showMessage = pyqtSignal(unicode) """Emitted when to show revision summary as a hint""" grepRequested = pyqtSignal(unicode, dict) """Emitted (pattern, opts) when user request to search changelog""" linkActivated = pyqtSignal(QString) """Emitted (path) when user clicks on link""" revsetFilterRequested = pyqtSignal(QString) """Ask the repowidget to change its revset filter""" runCustomCommandRequested = pyqtSignal(str, list) """Emitted when selects a custom tool on the context menu""" def canswitch(self): return False def __init__(self, repoagent, rev=None, parent=None): super(ManifestWidget, self).__init__(parent) self._repoagent = repoagent # TODO: replace by repoagent if setRepo(bundlerepo) can be removed self._repo = repoagent.rawRepo() self._rev = rev self._selectedrev = rev self._initwidget() self._initactions() self._setupmodel() self._setupfilterupdater() self._treeview.setCurrentIndex(self._treemodel.index(0, 0)) self.setRev(self._rev) def _initwidget(self): self.setLayout(QVBoxLayout()) self._splitter = QSplitter() self.layout().addWidget(self._splitter) self.layout().setContentsMargins(2, 2, 2, 2) navlayout = QVBoxLayout(spacing=0) navlayout.setContentsMargins(0, 0, 0, 0) self._toolbar = QToolBar() self._toolbar.setIconSize(QSize(16,16)) self._toolbar.setStyleSheet(qtlib.tbstylesheet) self._treeview = QManifestTreeView(self, headerHidden=True, dragEnabled=True) self._treeview.setContextMenuPolicy(Qt.CustomContextMenu) self._treeview.customContextMenuRequested.connect(self.menuRequest) self._treeview.doubleClicked.connect(self.onDoubleClick) navlayout.addWidget(self._toolbar) navlayout.addWidget(self._treeview) navlayoutw = QWidget() navlayoutw.setLayout(navlayout) self._splitter.addWidget(navlayoutw) self._splitter.setStretchFactor(0, 1) vbox = QVBoxLayout(spacing=0) vbox.setMargin(0) self.revpanel = revpanel.RevPanelWidget(self._repo) self.revpanel.linkActivated.connect(self.linkActivated) vbox.addWidget(self.revpanel, 0) self._fileview = fileview.HgFileView(self._repoagent, self) vbox.addWidget(self._fileview, 0) w = QWidget() w.setLayout(vbox) self._splitter.addWidget(w) self._splitter.setStretchFactor(1, 3) self._fileview.revisionSelected.connect(self.setRev) self._fileview.linkActivated.connect(self.linkActivated) for name in ('showMessage', 'grepRequested'): getattr(self._fileview, name).connect(getattr(self, name)) def loadSettings(self, qs, prefix): prefix += '/manifest' self._fileview.loadSettings(qs, prefix+'/fileview') self._splitter.restoreState(qs.value(prefix+'/splitter').toByteArray()) expanded = qs.value(prefix+'/revpanel.expanded', False).toBool() self.revpanel.set_expanded(expanded) def saveSettings(self, qs, prefix): prefix += '/manifest' self._fileview.saveSettings(qs, prefix+'/fileview') qs.setValue(prefix+'/splitter', self._splitter.saveState()) qs.setValue(prefix+'/revpanel.expanded', self.revpanel.is_expanded()) def _initactions(self): self.le = QManifestLineEdit() #QLineEdit() if hasattr(self.le, 'setPlaceholderText'): # Qt >= 4.7 self.le.setPlaceholderText(_('### filter text ###')) else: lbl = QLabel(_('Filter:')) self._toolbar.addWidget(lbl) self.le.keypressed.connect(self._treeview.setFocus) self._treeview.topreached.connect(self.le.setFocus) self._toolbar.addWidget(self.le) self._statusfilter = status.StatusFilterButton( statustext='MASC', text=_('Status')) self._toolbar.addWidget(self._statusfilter) self._fileactions = filectxactions.FilectxActions(self._repo, self, rev=self._rev) self._fileactions.linkActivated.connect(self.linkActivated) self._fileactions.filterRequested.connect(self.revsetFilterRequested) self._fileactions.runCustomCommandRequested.connect( self.runCustomCommandRequested) self.addActions(self._fileactions.actions()) def showEvent(self, event): QWidget.showEvent(self, event) if self._selectedrev != self._rev: # If the selected revision is not the same as the current revision # we must "reload" the manifest contents with the selected revision self.setRev(self._selectedrev) #@pyqtSlot(QModelIndex) def onDoubleClick(self, index): itemissubrepo = (self._treemodel.fileStatus(index) == 'S') if itemissubrepo: self._fileactions.opensubrepo() elif not self._treemodel.isDir(index): if self._treemodel.fileStatus(index) in 'C?': self._fileactions.editfile() else: self._fileactions.vdiff() def menuRequest(self, point): selmodel = self._treeview.selectionModel() if not selmodel.selectedRows(): return point = self._treeview.viewport().mapToGlobal(point) contextmenu = self._fileactions.menu() if contextmenu: contextmenu.exec_(point) #@pyqtSlot(QModelIndex) def _updateItemFileActions(self, index): itemissubrepo = (self._treemodel.fileStatus(index) == 'S') itemisdir = self._treemodel.isDir(index) self._fileactions.setPaths([self.path], itemissubrepo=itemissubrepo, itemisdir=itemisdir) @property def toolbar(self): """Return toolbar for manifest widget""" return self._toolbar @pyqtSlot(unicode, bool, bool, bool) def find(self, pattern, icase=False, wrap=False, forward=True): return self._fileview.find(pattern, icase, wrap, forward) @pyqtSlot(unicode, bool) def highlightText(self, pattern, icase=False): self._fileview.highlightText(pattern, icase) def _setupmodel(self): self._treemodel = ManifestModel(self._repo, self._rev, statusfilter=self._statusfilter.status(), parent=self) self._treemodel.setNameFilter(self.le.text()) oldmodel = self._treeview.model() oldselmodel = self._treeview.selectionModel() self._treeview.setModel(self._treemodel) if oldmodel: oldmodel.deleteLater() if oldselmodel: oldselmodel.deleteLater() selmodel = self._treeview.selectionModel() selmodel.currentChanged.connect(self._updatecontent) selmodel.currentChanged.connect(self._updateItemFileActions) selmodel.currentChanged.connect(self._emitPathChanged) self._statusfilter.statusChanged.connect(self._treemodel.setStatusFilter) self._statusfilter.statusChanged.connect(self._autoexpandtree) self._autoexpandtree() def _setupfilterupdater(self): self._filterupdatetimer = QTimer(self, interval=200, singleShot=True) self.le.returnPressed.connect(self._treeview.expandAll) self.le.textChanged.connect(self._filterupdatetimer.start) self._filterupdatetimer.timeout.connect(self._applyFilter) @pyqtSlot() def _applyFilter(self): filtertext = self.le.text() self._treemodel.setNameFilter(filtertext) self._treeview.enablefilterpalette(filtertext) @pyqtSlot() def _autoexpandtree(self): """expand file tree if the number of the items isn't large""" if 'C' not in self._statusfilter.status(): self._treeview.expandAll() def reload(self): # TODO pass def setRepo(self, repo): self._repo = repo #self._fileview.setRepo(repo) self._fileview.repo = repo if len(repo) <= self._rev: self._rev = len(repo)-1 self._setupmodel() self._fileactions.setRepo(repo) @property def rev(self): """Return current revision""" return self._rev def selectRev(self, rev): """ Select the revision that must be set when the dialog is shown again """ self._selectedrev = rev @pyqtSlot(int) @pyqtSlot(object) def setRev(self, rev): """Change revision to show""" self._selectedrev = rev self.revpanel.set_revision(rev) self.revpanel.update(repo = self._repo) if rev == self._rev: return self._rev = rev path = self.path self.revChanged.emit(rev) self._setupmodel() ctx = self._repo[rev] if path and hglib.fromunicode(path) in ctx: # recover file selection after reloading the model self.setPath(path) self._fileview.setContext(ctx) self._fileview.displayFile(self.path, self.status) self._fileactions.setRev(rev) @pyqtSlot(unicode, object) @pyqtSlot(unicode, object, int) def setSource(self, path, rev, line=None): """Change path and revision to show at once""" if self._rev != rev: self._rev = rev self._setupmodel() self._fileactions.setRev(rev) self.revChanged.emit(rev) if path != self.path: self.setPath(path) ctx = self._repo[rev] if hglib.fromunicode(self.path) in ctx: self._fileview.displayFile(path, self.status) else: self._fileview.clearDisplay() return if line: self._fileview.showLine(line - 1) @property def path(self): """Return currently selected path [unicode]""" return self._treemodel.filePath(self._treeview.currentIndex()) @property def status(self): """Return currently selected path""" return self._treemodel.fileStatus(self._treeview.currentIndex()) @pyqtSlot(unicode) def setPath(self, path): """Change path to show""" self._treeview.setCurrentIndex(self._treemodel.indexFromPath(path)) def displayFile(self): ctx, path = self._treemodel.fileSubrepoCtxFromPath(self.path) if ctx is None: ctx = self._repo[self._rev] else: ctx._repo.tabwidth = self._repo.tabwidth ctx._repo.maxdiff = self._repo.maxdiff self._fileview.setContext(ctx) self._fileview.displayFile(path, self.status) @pyqtSlot() def _updatecontent(self): self.displayFile() def setFileViewMode(self, mode): self._fileview.setMode(mode) @pyqtSlot() def _emitPathChanged(self): self.pathChanged.emit(self.path) def connectsearchbar(manifestwidget, searchbar): """Connect searchbar to manifest widget""" searchbar.conditionChanged.connect(manifestwidget.highlightText) searchbar.searchRequested.connect(manifestwidget.find) # In order to let the user seamlessly switch between the filterbox and the treeview # we subclas the QLineEdit and QTreeView widgets. We add some keypress related signals # will be used by the ManifestWidget to change the focus between these two widgets class QManifestLineEdit(QLineEdit): keypressed = pyqtSignal(int) def keyPressEvent(self, event): if event.key() == Qt.Key_Down: # must go down to the tree view self.keypressed.emit(event) else: # default handler for event super(QManifestLineEdit, self).keyPressEvent(event) class QManifestTreeView(QTreeView): topreached = pyqtSignal(int) def __init__(self, *args, **kwargs): QTreeView.__init__(self, *args, **kwargs) self._paletteswitcher = qtlib.PaletteSwitcher(self) def keyPressEvent(self, event): if self.currentIndex().row() == 0 \ and not self.currentIndex().parent().isValid(): if event.key() == Qt.Key_Up: # must go up to the filter box self.topreached.emit(event) return # default handler for event super(QManifestTreeView, self).keyPressEvent(event) def enablefilterpalette(self, enable): self._paletteswitcher.enablefilterpalette(enable) tortoisehg-2.10/tortoisehg/hgqt/sync.py0000644000076400007640000017756012231647662017402 0ustar stevesteve# sync.py - TortoiseHg's sync widget # # Copyright 2010 Adrian Buehlmann # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import tempfile from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import hg, ui, util, scmutil, httpconnection from tortoisehg.util import hglib, paths, wconfig from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, cmdui, thgrepo, rebase, resolve, hgrcutil from tortoisehg.hgqt import hgemail def parseurl(url): assert type(url) == unicode return util.url(hglib.fromunicode(url)) def linkify(url): assert type(url) == unicode u = util.url(hglib.fromunicode(url)) if u.scheme in ('local', 'http', 'https'): safe = util.hidepassword(hglib.fromunicode(url)) return u'%s' % (url, hglib.tounicode(safe)) else: return url class SyncWidget(QWidget, qtlib.TaskWidget): syncStarted = pyqtSignal() # incoming/outgoing/pull/push started outgoingNodes = pyqtSignal(object) incomingBundle = pyqtSignal(QString, QString) showMessage = pyqtSignal(unicode) pullCompleted = pyqtSignal() pushCompleted = pyqtSignal() output = pyqtSignal(QString, QString) progress = pyqtSignal(QString, object, QString, QString, object) makeLogVisible = pyqtSignal(bool) showBusyIcon = pyqtSignal(QString) hideBusyIcon = pyqtSignal(QString) switchToRequest = pyqtSignal(QString) def __init__(self, repoagent, parent, **opts): QWidget.__init__(self, parent) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) self.setLayout(layout) self.setAcceptDrops(True) self._repoagent = repoagent self.finishfunc = None self.opts = {} self.cmenu = None self.embedded = bool(parent) s = QSettings() for opt in ('subrepos', 'force', 'new-branch', 'noproxy', 'debug', 'mq'): val = s.value('sync/' + opt, None).toBool() if val: if opt != 'mq' or 'mq' in self.repo.extensions(): self.opts[opt] = val for opt in ('remotecmd', 'branch'): val = hglib.fromunicode(s.value('sync/' + opt, None).toString()) if val: self.opts[opt] = val self._repoagent.configChanged.connect(self.reload) if self.embedded: layout.setContentsMargins(2, 2, 2, 2) else: self.setWindowTitle(_('TortoiseHg Sync')) self.setWindowIcon(qtlib.geticon('thg-sync')) self.resize(850, 550) tb = QToolBar(self) tb.setStyleSheet(qtlib.tbstylesheet) self.layout().addWidget(tb) self.opbuttons = [] def newaction(tip, icon, cb): a = QAction(self) a.setToolTip(tip) a.setIcon(qtlib.geticon(icon)) a.triggered.connect(cb) self.opbuttons.append(a) tb.addAction(a) return a self.incomingAction = \ newaction(_('Check for incoming changes from selected URL'), 'hg-incoming', self.inclicked) self.pullAction = \ newaction(_('Pull incoming changes from selected URL'), 'hg-pull', lambda: self.pullclicked()) self.outgoingAction = \ newaction(_('Detect outgoing changes to selected URL'), 'hg-outgoing', self.outclicked) self.pushAction = \ newaction(_('Push outgoing changes to selected URL'), 'hg-push', lambda: self.pushclicked(None)) newaction(_('Email outgoing changesets for remote repository'), 'mail-forward', self.emailclicked) if 'perfarce' in self.repo.extensions(): a = QAction(self) a.setToolTip(_('Manage pending perforce changelists')) a.setText('P4') a.triggered.connect(self.p4pending) self.opbuttons.append(a) tb.addAction(a) tb.addSeparator() newaction(_('Unbundle'), 'hg-unbundle', self.unbundle) tb.addSeparator() self.stopAction = a = QAction(self) a.setToolTip(_('Stop current operation')) a.setIcon(qtlib.geticon('process-stop')) a.triggered.connect(self.stopclicked) a.setEnabled(False) tb.addAction(a) tb.addSeparator() self.optionsbutton = QPushButton(_('Options')) self.postpullbutton = QPushButton() tb.addWidget(self.postpullbutton) tb.addWidget(self.optionsbutton) self.targetcombo = QComboBox() self.targetcombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.targetcombo.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.targetcombo.setEnabled(False) self.targetcheckbox = QCheckBox(_('Target:')) self.targetcheckbox.toggled.connect(self.targetcombo.setEnabled) if self.embedded: tb.addSeparator() tb.addWidget(self.targetcheckbox) tb.addWidget(self.targetcombo) bottomlayout = QVBoxLayout() if not parent: bottomlayout.setContentsMargins(5, 5, 5, 5) else: bottomlayout.setContentsMargins(0, 0, 0, 0) layout.addLayout(bottomlayout) hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) bottomlayout.addLayout(hbox) self.optionshdrlabel = lbl = QLabel(_('Selected Options:')) hbox.addWidget(lbl) self.optionslabel = QLabel() self.optionslabel.setAcceptDrops(False) hbox.addWidget(self.optionslabel) hbox.addStretch() hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) bottomlayout.addLayout(hbox) self.pathEditToolbar = tbar = QToolBar(_('Path Edit Toolbar')) tbar.setStyleSheet(qtlib.tbstylesheet) tbar.setIconSize(QSize(16, 16)) hbox.addWidget(tbar) a = tbar.addAction(qtlib.geticon('thg-password'), _('Security')) a.setToolTip(_('Manage HTTPS connection security and user authentication')) self.securebutton = a tbar.addWidget(qtlib.Spacer(2, 2)) style = QApplication.style() a = tbar.addAction(style.standardIcon(QStyle.SP_DialogSaveButton), _('Save')) a.setToolTip(_('Save current URL under an alias')) self.savebutton = a tbar.addWidget(qtlib.Spacer(2, 2)) self.urlentry = QLineEdit() self.urlentry.textChanged.connect(self.urlChanged) tbar.addWidget(self.urlentry) # even though currentRowChanged fires pathSelected, clicked signal is # also connected to it. otherwise urlentry won't be updated when the # selection moves between hgrctv and reltv. hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) self.hgrctv = PathsTree(self, True) self.hgrctv.clicked.connect(self.pathSelected) self.hgrctv.removeAlias.connect(self.removeAlias) self.hgrctv.menuRequest.connect(self.menuRequest) pathsframe = QFrame() pathsframe.setFrameStyle(QFrame.StyledPanel|QFrame.Raised) pathsbox = QVBoxLayout() pathsbox.setContentsMargins(0, 0, 0, 0) pathsframe.setLayout(pathsbox) lbl = QLabel(_('Paths in Repository Settings:')) pathsbox.addWidget(lbl) pathsbox.addWidget(self.hgrctv) hbox.addWidget(pathsframe) self.reltv = PathsTree(self, False) self.reltv.clicked.connect(self.pathSelected) self.reltv.menuRequest.connect(self.menuRequest) self.reltv.clicked.connect(self.hgrctv.clearSelection) self.hgrctv.clicked.connect(self.reltv.clearSelection) pathsframe = QFrame() pathsframe.setFrameStyle(QFrame.StyledPanel|QFrame.Raised) pathsbox = QVBoxLayout() pathsbox.setContentsMargins(0, 0, 0, 0) pathsframe.setLayout(pathsbox) lbl = QLabel(_('Related Paths:')) pathsbox.addWidget(lbl) pathsbox.addWidget(self.reltv) hbox.addWidget(pathsframe) bottomlayout.addLayout(hbox, 1) self.savebutton.triggered.connect(self.saveclicked) self.securebutton.triggered.connect(self.secureclicked) self.postpullbutton.clicked.connect(self.postpullclicked) self.optionsbutton.pressed.connect(self.editOptions) cmd = cmdui.Widget(not self.embedded, True, self) cmd.commandStarted.connect(self.commandStarted) cmd.commandFinished.connect(self.commandFinished) cmd.makeLogVisible.connect(self.makeLogVisible) cmd.output.connect(self.outputHook) cmd.progress.connect(self.progress) if not self.embedded: self.showMessage.connect(cmd.stbar.showMessage) bottomlayout.addWidget(cmd) cmd.setVisible(False) self.cmd = cmd self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self) self.curalias = None self.reload() if 'default' in self.paths: self.setUrl('default') else: self.setEditUrl('') @property def repo(self): return self._repoagent.rawRepo() def canswitch(self): return not self.targetcheckbox.isChecked() def loadTargets(self, ctx): self.targetcombo.clear() # itemData(role=UserRole) is the argument list to pass to hg selIndex = 0 self.targetcombo.addItem(_('rev: %d (%s)') % (ctx.rev(), str(ctx)), ('--rev', str(ctx.rev()))) for name in self.repo.namedbranches: uname = hglib.tounicode(name) self.targetcombo.addItem(_('branch: ') + uname, ('--branch', name)) self.targetcombo.setItemData(self.targetcombo.count() - 1, name, Qt.ToolTipRole) if ctx.thgbranchhead() and name == ctx.branch(): selIndex = self.targetcombo.count() - 1 for name in self.repo._bookmarks.keys(): uname = hglib.tounicode(name) self.targetcombo.addItem(_('bookmark: ') + uname, ('--bookmark', name)) self.targetcombo.setItemData(self.targetcombo.count() - 1, name, Qt.ToolTipRole) if name in ctx.bookmarks(): selIndex = self.targetcombo.count() - 1 return selIndex def refreshTargets(self, rev): if type(rev) is not int: return if rev >= len(self.repo): return ctx = self.repo.changectx(rev) index = self.loadTargets(ctx) if index < 0: index = 0 self.targetcombo.setCurrentIndex(index) def isTargetSelected(self): return self.targetcheckbox.isChecked() def editOptions(self): dlg = OptionsDialog(self.opts, self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self.opts.update(dlg.outopts) self.refreshUrl() s = QSettings() for opt, val in self.opts.iteritems(): if isinstance(val, str): val = hglib.tounicode(val) s.setValue('sync/' + opt, val) @pyqtSlot() def reload(self): # Refresh configured paths self.paths = {} fn = self.repo.join('hgrc') fn, cfg = hgrcutil.loadIniFile([fn], self) if 'paths' in cfg: for alias in cfg['paths']: self.paths[ alias ] = cfg['paths'][ alias ] tm = PathsModel(self.paths.items(), self) self.hgrctv.setModel(tm) sm = self.hgrctv.selectionModel() sm.currentRowChanged.connect(self.pathSelected) # Refresh post-pull self.cachedpp = self.repo.postpull name = _('Post Pull: ') + self.repo.postpull.title() self.postpullbutton.setText(name) # Refresh related paths known = set() known.add(os.path.abspath(self.repo.root).lower()) for path in self.paths.values(): if not util.hasscheme(path): known.add(os.path.abspath(util.localpath(path)).lower()) else: known.add(path) related = {} for root, shortname in thgrepo.relatedRepositories(self.repo[0].node()): if root == self.repo.root: continue abs = os.path.abspath(root).lower() if abs not in known: related[root] = shortname known.add(abs) if root in thgrepo._repocache: # repositories already opened keep their ui instances in sync repo = thgrepo._repocache[root] ui = repo.ui elif paths.is_on_fixed_drive(root): # directly read the repository's configuration file tempui = self.repo.ui.copy() tempui.readconfig(os.path.join(root, '.hg', 'hgrc')) ui = tempui else: continue for alias, path in ui.configitems('paths'): if not util.hasscheme(path): abs = os.path.abspath(util.localpath(path)).lower() else: abs = path if abs not in known: related[path] = alias known.add(abs) pairs = [(alias, path) for path, alias in related.items()] tm = PathsModel(pairs, self) self.reltv.setModel(tm) sm = self.reltv.selectionModel() sm.currentRowChanged.connect(self.pathSelected) def currentUrl(self): return unicode(self.urlentry.text()) def urlChanged(self): self.securebutton.setEnabled('https://' in self.currentUrl()) def refreshUrl(self): 'User has selected a new URL' self.urlChanged() opts = [] for opt, value in self.opts.iteritems(): if value is True: opts.append('--'+opt) elif value: opts.append('--'+opt+'='+value) self.optionslabel.setText(hglib.tounicode(' '.join(opts))) self.optionslabel.setVisible(bool(opts)) self.optionshdrlabel.setVisible(bool(opts)) def pathSelected(self, index): aliasindex = index.sibling(index.row(), 0) alias = aliasindex.data(Qt.DisplayRole).toString() self.curalias = hglib.fromunicode(alias) path = index.model().realUrl(index) self.setEditUrl(hglib.tounicode(path)) def setEditUrl(self, newurl): 'Set the current URL without changing the alias [unicode]' self.urlentry.setText(newurl) self.refreshUrl() def setUrl(self, newurl): 'Set the current URL to the given alias or URL [unicode]' model = self.hgrctv.model() for col in (0, 1): # search known (alias, url) ixs = model.match(model.index(0, col), Qt.DisplayRole, newurl, 1, Qt.MatchFixedString | Qt.MatchCaseSensitive) if ixs: self.hgrctv.setCurrentIndex(ixs[0]) self.pathSelected(ixs[0]) # in case of row not changed return self.setEditUrl(newurl) def dragEnterEvent(self, event): data = event.mimeData() if data.hasUrls() or data.hasText(): event.setDropAction(Qt.CopyAction) event.acceptProposedAction() def dragMoveEvent(self, event): data = event.mimeData() if data.hasUrls() or data.hasText(): event.setDropAction(Qt.CopyAction) event.acceptProposedAction() def dropEvent(self, event): data = event.mimeData() if data.hasUrls(): url = unicode(data.urls()[0].toString()) event.setDropAction(Qt.CopyAction) event.accept() elif data.hasText(): url = unicode(data.text()) event.setDropAction(Qt.CopyAction) event.accept() else: return if url.startswith('file:///'): url = url[8:] self.setUrl(url) def canExit(self): return not self.cmd.core.running() @pyqtSlot(QPoint, QString, QString, bool) def menuRequest(self, point, url, alias, editable): 'menu event emitted by one of the two URL lists' if not self.cmenu: separator = (None, None, None) acts = [] menu = QMenu(self) for text, cb, icon in ( (_('E&xplore'), self.exploreurl, 'system-file-manager'), (_('&Terminal'), self.terminalurl, 'utilities-terminal'), (_('Copy &Path'), self.copypath, ''), separator, (_('&Edit...'), self.editurl, 'general'), (_('&Remove...'), self.removeurl, 'menudelete')): if text is None: menu.addSeparator() continue act = QAction(text, self) if icon: act.setIcon(qtlib.geticon(icon)) act.triggered.connect(cb) acts.append(act) menu.addAction(act) self.cmenu = menu self.acts = acts self.menuurl = url self.menualias = alias for act in self.acts[-2:]: act.setEnabled(editable) self.cmenu.exec_(point) def exploreurl(self): url = unicode(self.menuurl) u = parseurl(url) if not u.scheme or u.scheme == 'file': qtlib.openlocalurl(u.path) else: QDesktopServices.openUrl(QUrl(url)) def terminalurl(self): url = unicode(self.menuurl) u = parseurl(url) if u.scheme and u.scheme != 'file': qtlib.InfoMsgBox(_('Repository not local'), _('A terminal shell cannot be opened for remote')) return qtlib.openshell(u.path, 'repo ' + u.path) def editurl(self): alias = hglib.fromunicode(self.menualias) urlu = unicode(self.menuurl) dlg = SaveDialog(self.repo, alias, urlu, self, edit=True) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self.curalias = hglib.fromunicode(dlg.aliasentry.text()) self.setEditUrl(dlg.urlentry.text()) self.reload() def removeurl(self): if qtlib.QuestionMsgBox(_('Confirm path delete'), _('Delete %s from your repo configuration file?') % self.menualias, parent=self): self.removeAlias(self.menualias) def copypath(self): QApplication.clipboard().setText(self.menuurl) def closeEvent(self, event): if self.cmd.core.running(): if not qtlib.QuestionMsgBox(_('TortoiseHg Sync'), _('Are you sure that you want to cancel synchronization?'), parent=self): event.ignore() def keyPressEvent(self, event): if event.matches(QKeySequence.Refresh): self.reload() elif event.key() == Qt.Key_Escape: if self.cmd.core.running(): self.cmd.cancel() elif not self.embedded: self.close() else: return super(SyncWidget, self).keyPressEvent(event) def stopclicked(self): if self.cmd.core.running(): self.cmd.cancel() def saveclicked(self): if self.curalias: alias = self.curalias elif 'default' not in self.paths: alias = 'default' else: alias = 'new' dlg = SaveDialog(self.repo, alias, self.currentUrl(), self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self.curalias = hglib.fromunicode(dlg.aliasentry.text()) self.reload() def secureclicked(self): if not parseurl(self.currentUrl()).host: qtlib.WarningMsgBox(_('No host specified'), _('Please set a valid URL to continue.'), parent=self) return dlg = SecureDialog(self.repo, self.currentUrl(), self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.exec_() def commandStarted(self): for b in self.opbuttons: b.setEnabled(False) self.stopAction.setEnabled(True) if self.embedded: self.showBusyIcon.emit('thg-sync') else: self.cmd.setShowOutput(True) self.cmd.setVisible(True) def commandFinished(self, ret): self.hideBusyIcon.emit('thg-sync') self.repo.decrementBusyCount() for b in self.opbuttons: b.setEnabled(True) self.stopAction.setEnabled(False) if self.finishfunc: # allow GC to clean temp finishfunc. here we need to nullify it # before calling, because it may be reassigned in finishfunc(). f = self.finishfunc self.finishfunc = None output = self.cmd.core.rawoutput() f(ret, output) def run(self, cmdline, details): if self.cmd.core.running(): return self.lastcmdline = list(cmdline) for name in list(details) + ['remotecmd']: val = self.opts.get(name) if not val: continue if isinstance(val, bool): if val: cmdline.append('--' + name) elif val: cmdline.append('--' + name) cmdline.append(val) if 'rev' in details and '--rev' not in cmdline: if self.embedded and self.targetcheckbox.isChecked(): idx = self.targetcombo.currentIndex() if idx != -1: args = self.targetcombo.itemData(idx).toPyObject() if args[0][2:] not in details: args = ('--rev',) + args[1:] cmdline += args if self.opts.get('noproxy'): cmdline += ['--config', 'http_proxy.host='] if self.opts.get('debug'): cmdline.append('--debug') cururl = self.currentUrl() lurl = hglib.fromunicode(cururl) u = parseurl(cururl) if not u.host and not u.path: self.switchToRequest.emit('sync') qtlib.WarningMsgBox(_('No remote repository URL or path set'), _('No valid default remote repository URL or path ' 'has been configured for this repository.

Please type ' 'and save a remote repository path on the Sync widget.'), parent=self) return if u.scheme == 'https': if self.repo.ui.configbool('insecurehosts', u.host): cmdline.append('--insecure') if u.user: cleanurl = util.removeauth(lurl) res = httpconnection.readauthforuri(self.repo.ui, cleanurl, u.user) if res: group, auth = res if auth.get('username'): if qtlib.QuestionMsgBox( _('Redundant authentication info'), _('You have authentication info configured for ' 'this host and inside this URL. Remove ' 'authentication info from this URL?'), parent=self): self.setEditUrl(hglib.tounicode(cleanurl)) self.saveclicked() safeurl = util.hidepassword(lurl) display = ' '.join(cmdline + [safeurl]).replace('\n', '^M') if not self.opts.get('mq'): cmdline.append(lurl) self.repo.incrementBusyCount() self.cmd.run(cmdline, display=display, useproc='p4://' in lurl) @pyqtSlot(QString, QString) def outputHook(self, msg, label): label = unicode(label) if "'hg push --new-branch'" in msg and 'ui.error' in label.split(): # not report as error because it will be handled internally in the # same session (see pushclicked.finished) self.needNewBranch = True label = ' '.join(l for l in label.split() if l != 'ui.error') self.output.emit(msg, label) ## ## Workbench toolbar buttons ## def incoming(self): if self.cmd.core.running(): self.showMessage.emit(_('sync command already running')) else: self.inclicked() def pull(self): if self.cmd.core.running(): self.showMessage.emit(_('sync command already running')) else: self.pullclicked() def outgoing(self): if self.cmd.core.running(): self.showMessage.emit(_('sync command already running')) else: self.outclicked() def push(self, confirm, **kwargs): if self.cmd.core.running(): self.showMessage.emit(_('sync command already running')) else: self.pushclicked(confirm, **kwargs) def pullBundle(self, bundle, rev, bsource=None): 'accept bundle changesets' if self.cmd.core.running(): self.output.emit(_('sync command already running'), 'control') return save = self.currentUrl() orev = self.opts.get('rev') self.setEditUrl(hglib.tounicode(bundle)) if rev is not None: self.opts['rev'] = str(rev) self.pullclicked(bsource) self.setEditUrl(save) self.opts['rev'] = orev ## ## Sync dialog buttons ## def linkifyWithTarget(self, url): link = linkify(url) if self.embedded and self.targetcheckbox.isChecked(): link += u" (%s)" % self.targetcombo.currentText() return link def inclicked(self): self.syncStarted.emit() url = self.currentUrl() link = self.linkifyWithTarget(url) self.showMessage.emit(_('Getting incoming changesets from %s...') % link) if self.embedded and not url.startswith('p4://') and \ not self.opts.get('subrepos'): def finished(ret, output): if ret == 0 and os.path.exists(bfile): self.showMessage.emit(_('Found incoming changesets from %s') % link) self.incomingBundle.emit(hglib.tounicode(bfile), url) elif ret == 1: self.showMessage.emit(_('No incoming changesets from %s') % link) else: self.showMessage.emit(_('Incoming from %s aborted, ret %d') % (link, ret)) bfile = hglib.fromunicode(url) for badchar in (':', '*', '\\', '?', '#'): bfile = bfile.replace(badchar, '') bfile = bfile.replace('/', '_') bfile = tempfile.mktemp('.hg', bfile+'_', qtlib.gettempdir()) self.finishfunc = finished cmdline = ['--repository', self.repo.root, 'incoming', '--quiet', '--bundle', bfile] self.run(cmdline, ('force', 'branch', 'rev')) else: def finished(ret, output): if ret == 0: self.showMessage.emit(_('Found incoming changesets from %s') % link) elif ret == 1: self.showMessage.emit(_('No incoming changesets from %s') % link) else: self.showMessage.emit(_('Incoming from %s aborted, ret %d') % (link, ret)) self.finishfunc = finished cmdline = ['--repository', self.repo.root, 'incoming'] self.run(cmdline, ('force', 'branch', 'rev', 'subrepos')) def pullclicked(self, url=None): self.syncStarted.emit() link = self.linkifyWithTarget(url or self.currentUrl()) def finished(ret, output): if ret == 0: self.showMessage.emit(_('Pull from %s completed') % link) else: self.showMessage.emit(_('Pull from %s aborted, ret %d') % (link, ret)) self.pullCompleted.emit() # handle file conflicts during rebase if self.opts.get('rebase') or self.opts.get('updateorrebase'): if os.path.exists(self.repo.join('rebasestate')): dlg = rebase.RebaseDialog(self._repoagent, self) dlg.exec_() return # handle file conflicts during update for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': qtlib.InfoMsgBox(_('Merge caused file conflicts'), _('File conflicts need to be resolved')) dlg = resolve.ResolveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() return self.finishfunc = finished self.showMessage.emit(_('Pulling from %s...') % link) cmdline = ['--repository', self.repo.root, 'pull', '--verbose'] uimerge = self.repo.ui.configbool('tortoisehg', 'autoresolve') \ and 'ui.merge=internal:merge' or 'ui.merge=internal:fail' if self.cachedpp == 'rebase': cmdline += ['--rebase', '--config', uimerge] elif self.cachedpp == 'update': cmdline += ['--update', '--config', uimerge] elif self.cachedpp == 'updateorrebase': cmdline += ['--update', '--rebase', '--config', uimerge] elif self.cachedpp == 'fetch': cmdline[2] = 'fetch' elif self.opts.get('mq'): # force the tool to update to the pulled changeset cmdline += ['--update', '--config', uimerge] self.run(cmdline, ('force', 'branch', 'rev', 'bookmark', 'mq')) def outclicked(self): self.syncStarted.emit() link = self.linkifyWithTarget(self.currentUrl()) self.showMessage.emit(_('Finding outgoing changesets to %s...') % link) if self.embedded and not self.opts.get('subrepos'): def verifyhash(hash): if len(hash) != 40: return False bad = [c for c in hash if c not in '0123456789abcdef'] return not bad def outputnodes(ret, data): if ret == 0: nodes = [n for n in data.splitlines() if verifyhash(n)] if nodes: self.outgoingNodes.emit(nodes) self.showMessage.emit(_('%d outgoing changesets to %s') % (len(nodes), link)) elif ret == 1: self.showMessage.emit(_('No outgoing changesets to %s') % link) else: self.showMessage.emit(_('Outgoing to %s aborted, ret %d') % (link, ret)) self.finishfunc = outputnodes cmdline = ['--repository', self.repo.root, 'outgoing', '--quiet', '--template', '{node}\n'] self.run(cmdline, ('force', 'branch', 'rev')) else: def finished(ret, data): if ret == 0: self.showMessage.emit(_('outgoing changesets to %s found') % link) elif ret == 1: self.showMessage.emit(_('No outgoing changesets to %s') % link) else: self.showMessage.emit(_('Outgoing to %s aborted, ret %d') % (link, ret)) self.finishfunc = finished cmdline = ['--repository', self.repo.root, 'outgoing'] self.run(cmdline, ('force', 'branch', 'rev', 'subrepos')) def p4pending(self): p4url = hglib.fromunicode(self.currentUrl()) def finished(ret, output): pending = {} if ret == 0: for line in output.splitlines(): if line.startswith('ignoring hg revision'): continue try: hashes = line.split(' ') changelist = hashes.pop(0) clnum = int(changelist) if len(hashes)>1 and len(hashes[0])==1: state = hashes.pop(0) if state == 's': changelist = _('%s (submitted)') % changelist elif state == 'p': changelist = _('%s (pending)') % changelist else: raise ValueError pending[changelist] = hashes except (ValueError, IndexError): text = _('Unable to parse p4pending output') if pending: text = _('%d pending changelists found') % len(pending) else: text = _('No pending Perforce changelists') elif ret is None: text = _('Aborted p4pending') else: text = _('Unable to determine pending changesets') self.showMessage.emit(text) if pending: from tortoisehg.hgqt.p4pending import PerforcePending dlg = PerforcePending(self._repoagent, pending, p4url, self) dlg.showMessage.connect(self.showMessage) dlg.exec_() self.finishfunc = finished self.showMessage.emit(_('Perforce pending...')) self.run(['--repository', self.repo.root, 'p4pending', '--verbose'], ()) def pushclicked(self, confirm, rev=None, branch=None, pushall=False): if confirm is None: confirm = self.repo.ui.configbool('tortoisehg', 'confirmpush', True) if rev == '': rev = None if branch == '': branch = None if pushall and (rev is not None or branch is not None): raise ValueError('inconsistent call with pushall=%r, rev=%r and ' 'branch=%r' % (pushall, rev, branch)) validopts = ('force', 'new-branch', 'branch', 'rev', 'bookmark', 'mq') self.syncStarted.emit() lurl = hglib.fromunicode(self.currentUrl()) link = self.linkifyWithTarget(self.currentUrl()) if (not hg.islocal(lurl) and confirm and not self.targetcheckbox.isChecked()): r = qtlib.QuestionMsgBox(_('Confirm Push to remote Repository'), _('Push to remote repository\n%s\n?') % link, parent=self) if not r: self.showMessage.emit(_('Push to %s aborted') % link) self.pushCompleted.emit() return self.showMessage.emit(_('Pushing to %s...') % link) def finished(ret, output): if ret == 0: self.showMessage.emit(_('Push to %s completed') % link) elif ret == 1: self.showMessage.emit(_('No outgoing changesets to %s') % link) else: self.showMessage.emit(_('Push to %s aborted, ret %d') % (link, ret)) if self.needNewBranch and '--new-branch' not in self.lastcmdline: r = qtlib.QuestionMsgBox(_('Confirm New Branch'), _('One or more of the changesets that you ' 'are attempting to push involve the ' 'creation of a new branch. Do you want ' 'to create a new branch in the remote ' 'repository?'), parent=self) if r: cmdline = self.lastcmdline cmdline.extend(['--new-branch']) self.finishfunc = finished # should be called again self.run(cmdline, validopts) return self.pushCompleted.emit() self.finishfunc = finished if not pushall and rev is None and branch is None: # Read the tortoisehg.defaultpush setting to determine what to push by default defaultpush = self.repo.ui.config('tortoisehg', 'defaultpush', 'all') if self.targetcheckbox.isChecked(): # target selection overrides defaultpush pass elif defaultpush == 'all': # This is the default pass elif defaultpush == 'branch': branch = '.' elif defaultpush == 'revision': rev = '.' else: self.showMessage.emit(_('Invalid default push revision: %s. ' 'Please check your Mercurial configuration ' '(tortoisehg.defaultpush)') % defaultpush) self.pushCompleted.emit() return cmdline = ['--repository', self.repo.root, 'push'] if rev: cmdline.extend(['--rev', str(rev)]) if branch: cmdline.extend(['--branch', branch]) self.needNewBranch = False self.run(cmdline, validopts) def postpullclicked(self): dlg = PostPullDialog(self.repo, self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.exec_() def emailclicked(self): self.showMessage.emit(_('Determining outgoing changesets to email...')) def outputnodes(ret, data): if ret == 0: revs = tuple(self.repo[n].rev() for n in data.splitlines() if len(n) == 40) self.showMessage.emit(_('%d outgoing changesets') % len(revs)) try: outgoingrevs = (cmdline[cmdline.index('--rev') + 1],) except ValueError: outgoingrevs = None self._dialogs.open(SyncWidget._createEmailDialog, revs, outgoingrevs) elif ret == 1: self.showMessage.emit(_('No outgoing changesets')) else: self.showMessage.emit(_('Outgoing aborted, ret %d') % ret) self.finishfunc = outputnodes cmdline = ['--repository', self.repo.root, 'outgoing', '--quiet', '--template', '{node}\n'] self.run(cmdline, ('force', 'branch', 'rev')) def _createEmailDialog(self, revs, outgoingrevs): return hgemail.EmailDialog(self._repoagent, revs, outgoing=True, outgoingrevs=outgoingrevs) def unbundle(self): caption = _("Select bundle file") _FILE_FILTER = ';;'.join([_("Bundle files (*.hg)"), _("All files (*)")]) bundlefile = QFileDialog.getOpenFileName( self, caption, hglib.tounicode(self.repo.root), _FILE_FILTER) if bundlefile: # Set the pull source to the selected bundle file self.urlentry.setText(bundlefile) # Execute the incomming command, which will show the revisions in # the bundle, and let the user accept or reject them self.inclicked() @pyqtSlot(QString) def removeAlias(self, alias): alias = hglib.fromunicode(alias) fn = self.repo.join('hgrc') fn, cfg = hgrcutil.loadIniFile([fn], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to remove URL'), _('Iniparse must be installed.'), parent=self) return if fn is None: return if alias in cfg['paths']: del cfg['paths'][alias] self.repo.incrementBusyCount() try: wconfig.writefile(cfg, fn) except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) self.repo.decrementBusyCount() self.reload() class PostPullDialog(QDialog): def __init__(self, repo, parent): super(PostPullDialog, self).__init__(parent) self.repo = repo layout = QVBoxLayout() self.setLayout(layout) self.setWindowTitle(_('Post Pull Behavior')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) lbl = QLabel(_('Select post-pull operation for this repository')) layout.addWidget(lbl) self.none = QRadioButton(_('None - simply pull changesets')) self.update = QRadioButton(_('Update - pull, then try to update')) layout.addWidget(self.none) layout.addWidget(self.update) if 'fetch' in repo.extensions() or repo.postpull == 'fetch': if 'fetch' in repo.extensions(): btntxt = _('Fetch - use fetch (auto merge pulled changes)') else: btntxt = _('Fetch - use fetch extension (fetch is not active!)') self.fetch = QRadioButton(btntxt) layout.addWidget(self.fetch) else: self.fetch = None if ('rebase' in repo.extensions() or repo.postpull in ('rebase', 'updateorrebase')): if 'rebase' in repo.extensions(): rebasetxt = _('Rebase - rebase local commits above pulled changes') updateorrebasetxt = _('UpdateOrRebase - pull, then try to update or rebase') else: rebasetxt = _('Rebase - use rebase extension (rebase is not active!)') updateorrebasetxt = _('UpdateOrRebase - use rebase extension (rebase is not active!)') self.rebase = QRadioButton(rebasetxt) layout.addWidget(self.rebase) self.updateOrRebase = QRadioButton(updateorrebasetxt) layout.addWidget(self.updateOrRebase) self.none.setChecked(True) if repo.postpull == 'update': self.update.setChecked(True) elif repo.postpull == 'fetch': self.fetch.setChecked(True) elif repo.postpull == 'rebase': self.rebase.setChecked(True) elif repo.postpull == 'updateorrebase': self.updateOrRebase.setChecked(True) self.autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) self.autoresolve_chk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', False)) layout.addWidget(self.autoresolve_chk) cfglabel = QLabel(_('Launch settings tool...')) cfglabel.linkActivated.connect(self.linkactivated) layout.addWidget(cfglabel) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Save|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.bb = bb layout.addWidget(bb) def linkactivated(self, command): if command == 'config': from tortoisehg.hgqt.settings import SettingsDialog sd = SettingsDialog(configrepo=False, focus='tortoisehg.postpull', parent=self, root=self.repo.root) sd.exec_() def getValue(self): if self.none.isChecked(): return 'none' elif self.update.isChecked(): return 'update' elif (self.fetch and self.fetch.isChecked()): return 'fetch' elif (self.rebase and self.rebase.isChecked()): return 'rebase' else: return 'updateorrebase' def accept(self): path = self.repo.join('hgrc') fn, cfg = hgrcutil.loadIniFile([path], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save post pull operation'), _('Iniparse must be installed.'), parent=self) return if fn is None: return self.repo.incrementBusyCount() try: cfg.set('tortoisehg', 'postpull', self.getValue()) cfg.set('tortoisehg', 'autoresolve', self.autoresolve_chk.isChecked()) wconfig.writefile(cfg, fn) except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) self.repo.decrementBusyCount() super(PostPullDialog, self).accept() def reject(self): super(PostPullDialog, self).reject() class SaveDialog(QDialog): def __init__(self, repo, alias, urlu, parent, edit=False): super(SaveDialog, self).__init__(parent) self.setWindowTitle(_('Save Path')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.repo = repo self.origurl = hglib.fromunicode(urlu) self.setLayout(QFormLayout(fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow)) self.origalias = alias self.aliasentry = QLineEdit(hglib.tounicode(self.origalias)) self.aliasentry.selectAll() self.aliasentry.textChanged.connect(self.aliasChanged) self.layout().addRow(_('Alias'), self.aliasentry) safeurl = util.hidepassword(self.origurl) self.edit = edit if edit: self.urlentry = QLineEdit(urlu) self.urlentry.textChanged.connect(self.urlChanged) self.layout().addRow(_('URL'), self.urlentry) else: self.urllabel = QLabel(hglib.tounicode(safeurl)) self.layout().addRow(_('URL'), self.urllabel) u = parseurl(urlu) if not edit and (u.user or u.passwd) and u.scheme in ('http', 'https'): cleanurl = util.removeauth(self.origurl) def showurl(showclean): newurl = showclean and cleanurl or safeurl self.urllabel.setText(hglib.tounicode(newurl)) self.cleanurl = cleanurl self.clearcb = QCheckBox(_('Remove authentication data from URL')) self.clearcb.setToolTip( _('User authentication data should be associated with the ' 'hostname using the security dialog.')) self.clearcb.toggled.connect(showurl) self.clearcb.setChecked(True) self.layout().addRow(self.clearcb) else: self.clearcb = None s = QSettings() self.updatesubpaths = QCheckBox(_('Update subrepo paths')) self.updatesubpaths.setChecked( s.value('sync/updatesubpaths', True).toBool()) self.updatesubpaths.setToolTip( _('Update or create a path alias called \'%s\' on all subrepos, ' 'using this URL as the base URL, ' 'appending the local relative subrepo path to it') % hglib.tounicode(alias)) self.layout().addRow(self.updatesubpaths) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Save|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Save).setAutoDefault(True) self.bb = bb self.layout().addRow(None, bb) QTimer.singleShot(0, lambda:self.aliasentry.setFocus()) def savePath(self, repo, alias, path, confirm=True): fn = repo.join('hgrc') fn, cfg = hgrcutil.loadIniFile([fn], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save an URL'), _('Iniparse must be installed.'), parent=self) return if fn is None: return if confirm and (not self.edit or path != self.origurl) and alias in cfg['paths']: if not qtlib.QuestionMsgBox(_('Confirm URL replace'), _('%s already exists, replace URL?') % hglib.tounicode(alias), parent=self): return cfg.set('paths', alias, path) if self.edit and alias != self.origalias: cfg.remove('paths', self.origalias) try: wconfig.writefile(cfg, fn) except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) if self.updatesubpaths.isChecked(): ctx = repo['.'] for subname in ctx.substate: if ctx.substate[subname][2] != 'hg': continue if not os.path.exists(repo.wjoin(subname)): continue defaultsubpath = ctx.substate[subname][0] pathurl = util.url(path) if pathurl.scheme: subpath = str(pathurl).rstrip('/') + '/' + subname else: subpath = os.path.normpath(os.path.join(path, subname)) if defaultsubpath != subname: if not qtlib.QuestionMsgBox( _('Confirm URL replace'), _('Subrepo \'%s\' has a non trivial ' 'default sync URL:

%s

' 'Replace it with the following URL?:' '

%s') % (hglib.tounicode(subname), hglib.tounicode(defaultsubpath), hglib.tounicode(subpath)), parent=self): continue subrepo = hg.repository(repo.ui, path=repo.wjoin(subname)) self.savePath(subrepo, alias, subpath, confirm=False) def accept(self): alias = hglib.fromunicode(self.aliasentry.text()) if self.edit: path = hglib.fromunicode(self.urlentry.text()) elif self.clearcb and self.clearcb.isChecked(): path = self.cleanurl else: path = self.origurl self.repo.incrementBusyCount() self.savePath(self.repo, alias, path) self.repo.decrementBusyCount() s = QSettings() s.setValue('sync/updatesubpaths', self.updatesubpaths.isChecked()) super(SaveDialog, self).accept() def reject(self): super(SaveDialog, self).reject() def aliasChanged(self, text): enabled = len(text) > 0 if self.edit: enabled = enabled and len(self.urlentry.text()) > 0 self.bb.button(QDialogButtonBox.Save).setEnabled(enabled) def urlChanged(self, text): enabled = len(text) > 0 and len(self.aliasentry.text()) > 0 self.bb.button(QDialogButtonBox.Save).setEnabled(enabled) class SecureDialog(QDialog): def __init__(self, repo, urlu, parent): super(SecureDialog, self).__init__(parent) def genfingerprint(): if u.port is None: portnum = 443 else: try: portnum = int(u.port) except ValueError: qtlib.WarningMsgBox(_('Certificate Query Error'), _('Invalid port number: %s') % hglib.tounicode(u.port), parent=self) return try: pem = ssl.get_server_certificate( (u.host, portnum) ) der = ssl.PEM_cert_to_DER_cert(pem) except Exception, e: qtlib.WarningMsgBox(_('Certificate Query Error'), hglib.tounicode(str(e)), parent=self) return hash = util.sha1(der).hexdigest() pretty = ":".join([hash[x:x + 2] for x in xrange(0, len(hash), 2)]) le.setText(pretty) u = parseurl(urlu) assert u.host uhost = hglib.tounicode(u.host) self.setWindowTitle(_('Security: ') + uhost) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) # if the already user has an [auth] configuration for this URL, use it cleanurl = util.removeauth(hglib.fromunicode(urlu)) res = httpconnection.readauthforuri(repo.ui, cleanurl, u.user) if res: self.alias, auth = res else: self.alias, auth = u.host, {} self.repo = repo self.host = u.host if cleanurl.startswith('svn+https://'): self.schemes = 'svn+https' else: self.schemes = None self.setLayout(QVBoxLayout()) self.layout().addWidget(QLabel(_('Host: %s') % uhost)) securebox = QGroupBox(_('Secure HTTPS Connection')) self.layout().addWidget(securebox) vbox = QVBoxLayout() securebox.setLayout(vbox) self.layout().addWidget(securebox) self.cacertradio = QRadioButton( _('Verify with Certificate Authority certificates (best)')) self.fprintradio = QRadioButton( _('Verify with stored host fingerprint (good)')) self.insecureradio = QRadioButton( _('No host validation, but still encrypted (bad)')) hbox = QHBoxLayout() fprint = repo.ui.config('hostfingerprints', u.host, '') self.fprintentry = le = QLineEdit(fprint) self.fprintradio.toggled.connect(self.fprintentry.setEnabled) self.fprintentry.setEnabled(False) if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### host certificate fingerprint ###')) hbox.addWidget(le) try: import ssl # Python 2.6 or backport for 2.5 qb = QPushButton(_('Query')) qb.clicked.connect(genfingerprint) qb.setEnabled(False) self.fprintradio.toggled.connect(qb.setEnabled) hbox.addWidget(qb) except ImportError: pass vbox.addWidget(self.cacertradio) vbox.addWidget(self.fprintradio) vbox.addLayout(hbox) vbox.addWidget(self.insecureradio) self.cacertradio.setEnabled(bool(repo.ui.config('web', 'cacerts'))) self.cacertradio.setChecked(True) # default if fprint: self.fprintradio.setChecked(True) elif repo.ui.config('insecurehosts', u.host): self.insecureradio.setChecked(True) authbox = QGroupBox(_('User Authentication')) form = QFormLayout() authbox.setLayout(form) self.layout().addWidget(authbox) self.userentry = QLineEdit(u.user or auth.get('username', '')) self.userentry.setToolTip( _('''Optional. Username to authenticate with. If not given, and the remote site requires basic or digest authentication, the user will be prompted for it. Environment variables are expanded in the username letting you do foo.username = $USER.''')) form.addRow(_('Username'), self.userentry) self.pwentry = QLineEdit(u.passwd or auth.get('password', '')) self.pwentry.setEchoMode(QLineEdit.Password) self.pwentry.setToolTip( _('''Optional. Password to authenticate with. If not given, and the remote site requires basic or digest authentication, the user will be prompted for it.''')) form.addRow(_('Password'), self.pwentry) if 'mercurial_keyring' in repo.extensions(): self.pwentry.clear() self.pwentry.setEnabled(False) self.pwentry.setToolTip(_('Mercurial keyring extension is enabled. ' 'Passwords will be stored in a platform-native ' 'secure method.')) self.keyentry = QLineEdit(auth.get('key', '')) self.keyentry.setToolTip( _('''Optional. PEM encoded client certificate key file. Environment variables are expanded in the filename.''')) form.addRow(_('User Certificate Key File'), self.keyentry) self.chainentry = QLineEdit(auth.get('cert', '')) self.chainentry.setToolTip( _('''Optional. PEM encoded client certificate chain file. Environment variables are expanded in the filename.''')) form.addRow(_('User Certificate Chain File'), self.chainentry) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Help|BB.Save|BB.Cancel) bb.rejected.connect(self.reject) bb.accepted.connect(self.accept) bb.helpRequested.connect(self.keyringHelp) self.bb = bb self.layout().addWidget(bb) self.userentry.selectAll() QTimer.singleShot(0, lambda:self.userentry.setFocus()) def keyringHelp(self): qtlib.openhelpcontents('sync.html#security') def accept(self): path = scmutil.userrcpath() fn, cfg = hgrcutil.loadIniFile(path, self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save authentication'), _('Iniparse must be installed.'), parent=self) return if fn is None: return def setorclear(section, item, value): if value: cfg.set(section, item, value) elif not value and item in cfg[section]: del cfg[section][item] if self.cacertradio.isChecked(): fprint = None insecure = None elif self.fprintradio.isChecked(): fprint = hglib.fromunicode(self.fprintentry.text()) insecure = None else: fprint = None insecure = '1' setorclear('hostfingerprints', self.host, fprint) setorclear('insecurehosts', self.host, insecure) username = hglib.fromunicode(self.userentry.text()) password = hglib.fromunicode(self.pwentry.text()) key = hglib.fromunicode(self.keyentry.text()) chain = hglib.fromunicode(self.chainentry.text()) cfg.set('auth', self.alias+'.prefix', self.host) setorclear('auth', self.alias+'.username', username) setorclear('auth', self.alias+'.password', password) setorclear('auth', self.alias+'.key', key) setorclear('auth', self.alias+'.cert', chain) setorclear('auth', self.alias+'.schemes', self.schemes) self.repo.incrementBusyCount() try: wconfig.writefile(cfg, fn) except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) self.repo.decrementBusyCount() super(SecureDialog, self).accept() def reject(self): super(SecureDialog, self).reject() class PathsTree(QTreeView): removeAlias = pyqtSignal(QString) menuRequest = pyqtSignal(QPoint, QString, QString, bool) def __init__(self, parent, editable): QTreeView.__init__(self, parent) self.setDragDropMode(QTreeView.DragOnly) self.setSelectionMode(QTreeView.SingleSelection) self.editable = editable def contextMenuEvent(self, event): for index in self.selectedRows(): alias = index.data(Qt.DisplayRole).toString() url = index.sibling(index.row(), 1).data(Qt.DisplayRole).toString() self.menuRequest.emit(event.globalPos(), url, alias, self.editable) return def keyPressEvent(self, event): if self.editable and event.matches(QKeySequence.Delete): self.deleteSelected() else: return super(PathsTree, self).keyPressEvent(event) def deleteSelected(self): for index in self.selectedRows(): alias = index.data(Qt.DisplayRole).toString() r = qtlib.QuestionMsgBox(_('Confirm path delete'), _('Delete %s from your repo configuration file?') % alias, parent=self) if r: self.removeAlias.emit(alias) def selectedRows(self): return self.selectionModel().selectedRows() class PathsModel(QAbstractTableModel): def __init__(self, pathlist, parent=None): QAbstractTableModel.__init__(self, parent) self.headers = (_('Alias'), _('URL')) self.rows = [] for alias, path in sorted(pathlist): safepath = util.hidepassword(path) ualias = hglib.tounicode(alias) usafepath = hglib.tounicode(safepath) self.rows.append([ualias, usafepath, path]) def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self.rows) def columnCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self.headers) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role == Qt.DisplayRole: return QVariant(self.rows[index.row()][index.column()]) return QVariant() def headerData(self, col, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() else: return QVariant(self.headers[col]) def mimeData(self, indexes): urls = [] for i in indexes: u = QUrl() u.setPath(self.rows[i.row()][1]) urls.append(u) m = QMimeData() m.setUrls(urls) return m def mimeTypes(self): return ['text/uri-list'] def flags(self, index): flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled return flags def realUrl(self, index): return self.rows[index.row()][2] class OptionsDialog(QDialog): 'Utility dialog for configuring uncommon options' def __init__(self, opts, parent): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - sync options') % parent.repo.displayname) self.repo = parent.repo layout = QVBoxLayout() self.setLayout(layout) self.newbranchcb = QCheckBox( _('Allow push of a new branch (--new-branch)')) self.newbranchcb.setChecked(opts.get('new-branch', False)) layout.addWidget(self.newbranchcb) self.forcecb = QCheckBox( _('Force push or pull (override safety checks, --force)')) self.forcecb.setChecked(opts.get('force', False)) layout.addWidget(self.forcecb) self.subrepocb = QCheckBox( _('Recurse into subrepositories') + u' (--subrepos)') self.subrepocb.setChecked(opts.get('subrepos', False)) layout.addWidget(self.subrepocb) self.noproxycb = QCheckBox( _('Temporarily disable configured HTTP proxy')) self.noproxycb.setChecked(opts.get('noproxy', False)) layout.addWidget(self.noproxycb) proxy = self.repo.ui.config('http_proxy', 'host') self.noproxycb.setEnabled(bool(proxy)) self.debugcb = QCheckBox( _('Emit debugging output (--debug)')) self.debugcb.setChecked(opts.get('debug', False)) layout.addWidget(self.debugcb) if 'mq' in self.repo.extensions(): self.mqcb = QCheckBox( _('Work on patch queue (--mq)')) self.mqcb.setChecked(opts.get('mq', False)) layout.addWidget(self.mqcb) form = QFormLayout() layout.addLayout(form) lbl = QLabel(_('Remote command:')) self.remotele = QLineEdit() if opts.get('remotecmd'): self.remotele.setText(hglib.tounicode(opts['remotecmd'])) form.addRow(lbl, self.remotele) lbl = QLabel(_('Branch:')) self.branchle = QLineEdit() if opts.get('branch'): self.branchle.setText(hglib.tounicode(opts['branch'])) form.addRow(lbl, self.branchle) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.bb = bb layout.addWidget(bb) def accept(self): outopts = {} for name, le in (('remotecmd', self.remotele), ('branch', self.branchle)): outopts[name] = hglib.fromunicode(le.text()).strip() outopts['subrepos'] = self.subrepocb.isChecked() outopts['force'] = self.forcecb.isChecked() outopts['new-branch'] = self.newbranchcb.isChecked() outopts['noproxy'] = self.noproxycb.isChecked() outopts['debug'] = self.debugcb.isChecked() if 'mq' in self.repo.extensions(): outopts['mq'] = self.mqcb.isChecked() self.outopts = outopts QDialog.accept(self) tortoisehg-2.10/tortoisehg/hgqt/hgemail_ui.py0000644000076400007640000004417512212224146020507 0ustar stevesteve# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/steve/repos/thg/tortoisehg/hgqt/hgemail.ui' # # Created: Thu Sep 5 19:57:10 2013 # by: PyQt4 UI code generator 4.6.2 # # WARNING! All changes made in this file will be lost! from tortoisehg.hgqt.i18n import _ from PyQt4 import QtCore, QtGui class Ui_EmailDialog(object): def setupUi(self, EmailDialog): EmailDialog.setObjectName("EmailDialog") EmailDialog.resize(660, 519) EmailDialog.setSizeGripEnabled(True) self.verticalLayout_5 = QtGui.QVBoxLayout(EmailDialog) self.verticalLayout_5.setObjectName("verticalLayout_5") self.main_tabs = QtGui.QTabWidget(EmailDialog) self.main_tabs.setDocumentMode(False) self.main_tabs.setTabsClosable(False) self.main_tabs.setMovable(False) self.main_tabs.setObjectName("main_tabs") self.edit_tab = QtGui.QWidget() self.edit_tab.setObjectName("edit_tab") self.gridLayout = QtGui.QGridLayout(self.edit_tab) self.gridLayout.setObjectName("gridLayout") self.envelope_box = QtGui.QGroupBox(self.edit_tab) self.envelope_box.setObjectName("envelope_box") self.formLayout = QtGui.QFormLayout(self.envelope_box) self.formLayout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow) self.formLayout.setObjectName("formLayout") self.to_label = QtGui.QLabel(self.envelope_box) self.to_label.setObjectName("to_label") self.formLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.to_label) self.to_edit = QtGui.QComboBox(self.envelope_box) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.to_edit.sizePolicy().hasHeightForWidth()) self.to_edit.setSizePolicy(sizePolicy) self.to_edit.setEditable(True) self.to_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.to_edit.setObjectName("to_edit") self.formLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.to_edit) self.cc_label = QtGui.QLabel(self.envelope_box) self.cc_label.setObjectName("cc_label") self.formLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.cc_label) self.cc_edit = QtGui.QComboBox(self.envelope_box) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.cc_edit.sizePolicy().hasHeightForWidth()) self.cc_edit.setSizePolicy(sizePolicy) self.cc_edit.setEditable(True) self.cc_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.cc_edit.setObjectName("cc_edit") self.formLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.cc_edit) self.from_label = QtGui.QLabel(self.envelope_box) self.from_label.setObjectName("from_label") self.formLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.from_label) self.from_edit = QtGui.QComboBox(self.envelope_box) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.from_edit.sizePolicy().hasHeightForWidth()) self.from_edit.setSizePolicy(sizePolicy) self.from_edit.setEditable(True) self.from_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.from_edit.setObjectName("from_edit") self.formLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.from_edit) self.inreplyto_label = QtGui.QLabel(self.envelope_box) self.inreplyto_label.setObjectName("inreplyto_label") self.formLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.inreplyto_label) self.inreplyto_edit = QtGui.QLineEdit(self.envelope_box) self.inreplyto_edit.setObjectName("inreplyto_edit") self.formLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.inreplyto_edit) self.flag_label = QtGui.QLabel(self.envelope_box) self.flag_label.setObjectName("flag_label") self.formLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.flag_label) self.flag_edit = QtGui.QComboBox(self.envelope_box) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.flag_edit.sizePolicy().hasHeightForWidth()) self.flag_edit.setSizePolicy(sizePolicy) self.flag_edit.setEditable(True) self.flag_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.flag_edit.setObjectName("flag_edit") self.formLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.flag_edit) self.gridLayout.addWidget(self.envelope_box, 0, 0, 1, 1) self.options_edit = QtGui.QGroupBox(self.edit_tab) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.options_edit.sizePolicy().hasHeightForWidth()) self.options_edit.setSizePolicy(sizePolicy) self.options_edit.setObjectName("options_edit") self.verticalLayout_4 = QtGui.QVBoxLayout(self.options_edit) self.verticalLayout_4.setObjectName("verticalLayout_4") self.patch_frame = QtGui.QFrame(self.options_edit) self.patch_frame.setFrameShape(QtGui.QFrame.NoFrame) self.patch_frame.setFrameShadow(QtGui.QFrame.Raised) self.patch_frame.setObjectName("patch_frame") self.verticalLayout = QtGui.QVBoxLayout(self.patch_frame) self.verticalLayout.setObjectName("verticalLayout") self.hgpatch_radio = QtGui.QRadioButton(self.patch_frame) self.hgpatch_radio.setObjectName("hgpatch_radio") self.verticalLayout.addWidget(self.hgpatch_radio) self.gitpatch_radio = QtGui.QRadioButton(self.patch_frame) self.gitpatch_radio.setObjectName("gitpatch_radio") self.verticalLayout.addWidget(self.gitpatch_radio) self.plainpatch_radio = QtGui.QRadioButton(self.patch_frame) self.plainpatch_radio.setObjectName("plainpatch_radio") self.verticalLayout.addWidget(self.plainpatch_radio) self.bundle_radio = QtGui.QRadioButton(self.patch_frame) self.bundle_radio.setObjectName("bundle_radio") self.verticalLayout.addWidget(self.bundle_radio) self.verticalLayout_4.addWidget(self.patch_frame) self.extra_frame = QtGui.QFrame(self.options_edit) self.extra_frame.setFrameShape(QtGui.QFrame.NoFrame) self.extra_frame.setFrameShadow(QtGui.QFrame.Raised) self.extra_frame.setObjectName("extra_frame") self.horizontalLayout = QtGui.QHBoxLayout(self.extra_frame) self.horizontalLayout.setObjectName("horizontalLayout") self.body_check = QtGui.QCheckBox(self.extra_frame) self.body_check.setEnabled(True) self.body_check.setChecked(True) self.body_check.setObjectName("body_check") self.horizontalLayout.addWidget(self.body_check) self.attach_check = QtGui.QCheckBox(self.extra_frame) self.attach_check.setObjectName("attach_check") self.horizontalLayout.addWidget(self.attach_check) self.inline_check = QtGui.QCheckBox(self.extra_frame) self.inline_check.setObjectName("inline_check") self.horizontalLayout.addWidget(self.inline_check) self.diffstat_check = QtGui.QCheckBox(self.extra_frame) self.diffstat_check.setObjectName("diffstat_check") self.horizontalLayout.addWidget(self.diffstat_check) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.verticalLayout_4.addWidget(self.extra_frame) self.gridLayout.addWidget(self.options_edit, 0, 1, 1, 1) self.writeintro_check = QtGui.QCheckBox(self.edit_tab) self.writeintro_check.setObjectName("writeintro_check") self.gridLayout.addWidget(self.writeintro_check, 1, 0, 1, 2) self.intro_changesets_splitter = QtGui.QSplitter(self.edit_tab) self.intro_changesets_splitter.setOrientation(QtCore.Qt.Vertical) self.intro_changesets_splitter.setObjectName("intro_changesets_splitter") self.intro_box = QtGui.QGroupBox(self.intro_changesets_splitter) self.intro_box.setObjectName("intro_box") self.verticalLayout_2 = QtGui.QVBoxLayout(self.intro_box) self.verticalLayout_2.setObjectName("verticalLayout_2") self.subject_layout = QtGui.QHBoxLayout() self.subject_layout.setObjectName("subject_layout") self.subject_label = QtGui.QLabel(self.intro_box) self.subject_label.setObjectName("subject_label") self.subject_layout.addWidget(self.subject_label) self.subject_edit = QtGui.QComboBox(self.intro_box) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.subject_edit.sizePolicy().hasHeightForWidth()) self.subject_edit.setSizePolicy(sizePolicy) self.subject_edit.setEditable(True) self.subject_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.subject_edit.setObjectName("subject_edit") self.subject_layout.addWidget(self.subject_edit) self.verticalLayout_2.addLayout(self.subject_layout) self.body_edit = QtGui.QPlainTextEdit(self.intro_box) font = QtGui.QFont() font.setFamily("Monospace") self.body_edit.setFont(font) self.body_edit.setObjectName("body_edit") self.verticalLayout_2.addWidget(self.body_edit) self.changesets_box = QtGui.QGroupBox(self.intro_changesets_splitter) self.changesets_box.setObjectName("changesets_box") self.verticalLayout_3 = QtGui.QVBoxLayout(self.changesets_box) self.verticalLayout_3.setObjectName("verticalLayout_3") self.changesets_view = QtGui.QTreeView(self.changesets_box) self.changesets_view.setIndentation(0) self.changesets_view.setRootIsDecorated(False) self.changesets_view.setItemsExpandable(False) self.changesets_view.setObjectName("changesets_view") self.verticalLayout_3.addWidget(self.changesets_view) self.selectallnone_layout = QtGui.QHBoxLayout() self.selectallnone_layout.setObjectName("selectallnone_layout") self.selectall_button = QtGui.QPushButton(self.changesets_box) self.selectall_button.setObjectName("selectall_button") self.selectallnone_layout.addWidget(self.selectall_button) self.selectnone_button = QtGui.QPushButton(self.changesets_box) self.selectnone_button.setObjectName("selectnone_button") self.selectallnone_layout.addWidget(self.selectnone_button) spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.selectallnone_layout.addItem(spacerItem1) self.verticalLayout_3.addLayout(self.selectallnone_layout) self.gridLayout.addWidget(self.intro_changesets_splitter, 2, 0, 1, 2) self.main_tabs.addTab(self.edit_tab, "") self.preview_tab = QtGui.QWidget() self.preview_tab.setObjectName("preview_tab") self.gridLayout_2 = QtGui.QGridLayout(self.preview_tab) self.gridLayout_2.setObjectName("gridLayout_2") self.preview_edit = Qsci.QsciScintilla(self.preview_tab) self.preview_edit.setObjectName("preview_edit") self.gridLayout_2.addWidget(self.preview_edit, 0, 0, 1, 1) self.main_tabs.addTab(self.preview_tab, "") self.verticalLayout_5.addWidget(self.main_tabs) self.dialogbuttons_layout = QtGui.QHBoxLayout() self.dialogbuttons_layout.setObjectName("dialogbuttons_layout") self.settings_button = QtGui.QPushButton(EmailDialog) self.settings_button.setDefault(False) self.settings_button.setObjectName("settings_button") self.dialogbuttons_layout.addWidget(self.settings_button) spacerItem2 = QtGui.QSpacerItem(25, 19, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem2) self.send_button = QtGui.QPushButton(EmailDialog) self.send_button.setEnabled(False) self.send_button.setDefault(False) self.send_button.setObjectName("send_button") self.dialogbuttons_layout.addWidget(self.send_button) self.close_button = QtGui.QPushButton(EmailDialog) self.close_button.setEnabled(True) self.close_button.setDefault(True) self.close_button.setObjectName("close_button") self.dialogbuttons_layout.addWidget(self.close_button) self.verticalLayout_5.addLayout(self.dialogbuttons_layout) self.to_label.setBuddy(self.to_edit) self.cc_label.setBuddy(self.cc_edit) self.from_label.setBuddy(self.from_edit) self.inreplyto_label.setBuddy(self.inreplyto_edit) self.flag_label.setBuddy(self.flag_edit) self.subject_label.setBuddy(self.subject_edit) self.retranslateUi(EmailDialog) self.main_tabs.setCurrentIndex(0) QtCore.QObject.connect(self.writeintro_check, QtCore.SIGNAL("toggled(bool)"), self.intro_box.setVisible) QtCore.QObject.connect(self.send_button, QtCore.SIGNAL("clicked()"), EmailDialog.accept) QtCore.QObject.connect(self.close_button, QtCore.SIGNAL("clicked()"), EmailDialog.close) QtCore.QObject.connect(self.writeintro_check, QtCore.SIGNAL("toggled(bool)"), self.subject_edit.setFocus) QtCore.QMetaObject.connectSlotsByName(EmailDialog) EmailDialog.setTabOrder(self.main_tabs, self.to_edit) EmailDialog.setTabOrder(self.to_edit, self.cc_edit) EmailDialog.setTabOrder(self.cc_edit, self.from_edit) EmailDialog.setTabOrder(self.from_edit, self.inreplyto_edit) EmailDialog.setTabOrder(self.inreplyto_edit, self.flag_edit) EmailDialog.setTabOrder(self.flag_edit, self.hgpatch_radio) EmailDialog.setTabOrder(self.hgpatch_radio, self.gitpatch_radio) EmailDialog.setTabOrder(self.gitpatch_radio, self.plainpatch_radio) EmailDialog.setTabOrder(self.plainpatch_radio, self.bundle_radio) EmailDialog.setTabOrder(self.bundle_radio, self.body_check) EmailDialog.setTabOrder(self.body_check, self.attach_check) EmailDialog.setTabOrder(self.attach_check, self.inline_check) EmailDialog.setTabOrder(self.inline_check, self.diffstat_check) EmailDialog.setTabOrder(self.diffstat_check, self.writeintro_check) EmailDialog.setTabOrder(self.writeintro_check, self.subject_edit) EmailDialog.setTabOrder(self.subject_edit, self.body_edit) EmailDialog.setTabOrder(self.body_edit, self.changesets_view) EmailDialog.setTabOrder(self.changesets_view, self.send_button) EmailDialog.setTabOrder(self.send_button, self.preview_edit) EmailDialog.setTabOrder(self.preview_edit, self.settings_button) def retranslateUi(self, EmailDialog): EmailDialog.setWindowTitle(_('Email')) self.to_label.setText(_('To:')) self.cc_label.setText(_('Cc:')) self.from_label.setText(_('From:')) self.inreplyto_label.setText(_('In-Reply-To:')) self.inreplyto_edit.setToolTip(_('Message identifier to reply to, for threading')) self.flag_label.setText(_('Flag:')) self.hgpatch_radio.setWhatsThis(_('Hg patches (as generated by export command) are compatible with most patch programs. They include a header which contains the most important changeset metadata.')) self.hgpatch_radio.setText(_('Send changesets as Hg patches')) self.gitpatch_radio.setWhatsThis(_('Git patches can describe binary files, copies, and permission changes, but recipients may not be able to use them if they are not using git or Mercurial.')) self.gitpatch_radio.setText(_('Use extended (git) patch format')) self.plainpatch_radio.setWhatsThis(_('Stripping Mercurial header removes username and parent information. Only useful if recipient is not using Mercurial (and does not like to see the headers).')) self.plainpatch_radio.setText(_('Plain, do not prepend Hg header')) self.bundle_radio.setWhatsThis(_('Bundles store complete changesets in binary form. Upstream users can pull from them. This is the safest way to send changes to recipient Mercurial users.')) self.bundle_radio.setText(_('Send single binary bundle, not patches')) self.body_check.setToolTip(_('send patches as part of the email body')) self.body_check.setText(_('body')) self.attach_check.setToolTip(_('send patches as attachments')) self.attach_check.setText(_('attach')) self.inline_check.setToolTip(_('send patches as inline attachments')) self.inline_check.setText(_('inline')) self.diffstat_check.setToolTip(_('add diffstat output to messages')) self.diffstat_check.setText(_('diffstat')) self.writeintro_check.setWhatsThis(_('Patch series description is sent in initial summary email with [PATCH 0 of N] subject. It should describe the effects of the entire patch series. When emailing a bundle, these fields make up the message subject and body. Flags is a comma separated list of tags which are inserted into the message subject prefix.')) self.writeintro_check.setText(_('Write patch series (bundle) description')) self.subject_label.setText(_('Subject:')) self.changesets_box.setTitle(_('Changesets')) self.selectall_button.setText(_('Select &All')) self.selectnone_button.setText(_('Select &None')) self.main_tabs.setTabText(self.main_tabs.indexOf(self.edit_tab), _('Edit')) self.main_tabs.setTabText(self.main_tabs.indexOf(self.preview_tab), _('Preview')) self.settings_button.setText(_('&Settings')) self.send_button.setText(_('Send &Email')) self.close_button.setText(_('&Close')) from PyQt4 import Qsci tortoisehg-2.10/tortoisehg/hgqt/cslist.py0000644000076400007640000001501612110205646017676 0ustar stevesteve# cslist.py - embeddable changeset/patch list component # # Copyright 2009 Yuki KODAMA # Copyright 2010 David Wilhelm # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import hg from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.hgqt import csinfo, qtlib, thgrepo from tortoisehg.hgqt.i18n import _ from tortoisehg.util.patchctx import patchctx _SPACING = 6 class ChangesetList(QWidget): def __init__(self, repo=None, parent=None): super(ChangesetList, self).__init__() self.currepo = repo self.curitems = None self.curfactory = None self.showitems = None self.limit = 20 contents = ('%(item_l)s:', ' %(branch)s', ' %(tags)s', ' %(summary)s') self.lstyle = csinfo.labelstyle(contents=contents, width=350, selectable=True) contents = ('item', 'summary', 'user', 'dateage', 'rawbranch', 'tags', 'graft', 'transplant', 'p4', 'svn', 'converted') self.pstyle = csinfo.panelstyle(contents=contents, width=350, selectable=True) # main layout self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.mainvbox = QVBoxLayout() self.mainvbox.setSpacing(_SPACING) self.mainvbox.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(self.mainvbox) ## status box self.statusbox = QHBoxLayout() self.statuslabel = QLabel(_('No items to display')) self.compactchk = QCheckBox(_('Use compact view')) self.statusbox.addWidget(self.statuslabel) self.statusbox.addWidget(self.compactchk) self.mainvbox.addLayout(self.statusbox) ## scroll area self.scrollarea = QScrollArea() self.scrollarea.setMinimumSize(400, 200) self.scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scrollarea.setWidgetResizable(True) self.mainvbox.addWidget(self.scrollarea) ### cs layout grid, contains Factory objects, one per revision self.scrollbox = QWidget() self.csvbox = QVBoxLayout() self.csvbox.setSpacing(_SPACING) self.csvbox.setSizeConstraint(QLayout.SetMaximumSize) self.scrollbox.setLayout(self.csvbox) self.scrollarea.setWidget(self.scrollbox) # signal handlers self.compactchk.toggled.connect(lambda *a: self.update(self.curitems)) # csetinfo def datafunc(widget, item, ctx): if item in ('item', 'item_l'): if not isinstance(ctx, patchctx): return True revid = widget.get_data('revid') if not revid: return widget.target filename = os.path.basename(widget.target) return filename, revid raise csinfo.UnknownItem(item) def labelfunc(widget, item, ctx): if item in ('item', 'item_l'): if not isinstance(ctx, patchctx): return _('Revision:') return _('Patch:') raise csinfo.UnknownItem(item) def markupfunc(widget, item, value): if item in ('item', 'item_l'): if not isinstance(widget.ctx, patchctx): if item == 'item': return widget.get_markup('rev') return widget.get_markup('revnum') mono = dict(face='monospace', size='9000') if isinstance(value, basestring): return qtlib.markup(value, **mono) filename = qtlib.markup(value[0]) revid = qtlib.markup(value[1], **mono) if item == 'item': return '%s (%s)' % (filename, revid) return filename raise csinfo.UnknownItem(item) self.custom = csinfo.custom(data=datafunc, label=labelfunc, markup=markupfunc) def clear(self): """Clear the item list""" while self.csvbox.count(): w = self.csvbox.takeAt(0).widget() w.deleteLater() self.curitems = None def insertcs(self, item): """Insert changeset info into the item list. item: String, revision number or patch file path to display. """ style = self.compactchk.isChecked() and self.lstyle or self.pstyle info = self.curfactory(item, style=style) info.update(item) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) info.setSizePolicy(sizePolicy) self.csvbox.addWidget(info, Qt.AlignTop) def updatestatus(self): if self.curitems is None: text = _('No items to display') else: num = dict(count=len(self.showitems), total=len(self.curitems)) text = _('Displaying %(count)d of %(total)d items') % num self.statuslabel.setText(text) def update(self, items, uselimit=True): """Update the item list. Public arguments: items: List of revision numbers and/or patch file paths. You can pass a mixed list. The order will be respected. uselimit: If True, some of items will be shown. return: True if the item list was updated successfully, False if it wasn't updated. """ # setup self.clear() self.curfactory = csinfo.factory(self.currepo, self.custom) # initialize variables self.curitems = items if not items or not self.currepo: self.updatestatus() return False if self.compactchk.isChecked(): self.csvbox.setSpacing(0) else: self.csvbox.setSpacing(_SPACING) # determine the items to show if uselimit and self.limit < len(items): showitems, lastitem = items[:self.limit - 1], items[-1] else: showitems, lastitem = items, None numshow = len(showitems) + (lastitem and 1 or 0) self.showitems = showitems + (lastitem and [lastitem] or []) # show items for item in showitems: self.insertcs(item) if lastitem: self.csvbox.addWidget(QLabel("...")) self.insertcs(lastitem) self.updatestatus() return True tortoisehg-2.10/tortoisehg/hgqt/htmldelegate.py0000664000076400007640000000451312100577421021040 0ustar stevesteve# htmldelegate.py - HTML QStyledItemDelegate # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from PyQt4.QtCore import * from PyQt4.QtGui import * class HTMLDelegate(QStyledItemDelegate): def __init__(self, parent=0, cols=None): QStyledItemDelegate.__init__(self, parent) self.cols = cols def paint(self, painter, option, index): if self.cols and index.column() not in self.cols: return QStyledItemDelegate.paint(self, painter, option, index) # draw selection option = QStyleOptionViewItemV4(option) self.parent().style().drawControl(QStyle.CE_ItemViewItem, option, painter) # draw text doc = self._builddoc(option, index) painter.save() painter.setClipRect(option.rect) painter.translate(QPointF( option.rect.left(), option.rect.top() + (option.rect.height() - doc.size().height()) / 2)) ctx = QAbstractTextDocumentLayout.PaintContext() ctx.palette = option.palette if option.state & QStyle.State_Selected: if option.state & QStyle.State_Active: ctx.palette.setCurrentColorGroup(QPalette.Active) else: ctx.palette.setCurrentColorGroup(QPalette.Inactive) ctx.palette.setBrush(QPalette.Text, ctx.palette.highlightedText()) elif not option.state & QStyle.State_Enabled: ctx.palette.setCurrentColorGroup(QPalette.Disabled) doc.documentLayout().draw(painter, ctx) painter.restore() def sizeHint(self, option, index): doc = self._builddoc(option, index) return QSize(doc.idealWidth() + 5, doc.size().height()) def _builddoc(self, option, index): try: text = index.model().data(index, Qt.DisplayRole).toString() except error.RevlogError, e: # this can happen if revlog is being truncated while we read it text = _('?? Error: %s ??') % hglib.tounicode(str(e)) doc = QTextDocument(defaultFont=option.font) doc.setHtml(text) return doc tortoisehg-2.10/tortoisehg/hgqt/filerevmodel.py0000644000076400007640000000647112170335562021066 0ustar stevesteve# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. from mercurial import error from tortoisehg.hgqt.repomodel import HgRepoListModel, COLUMNHEADERS from tortoisehg.hgqt.graph import Graph, filelog_grapher from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import * FILE_HEADERS = (('Filename', _('Filename', 'column header')),) UNUSED_HEADERS = ('Changes') FILE_COLUMNHEADERS = tuple(c for c in COLUMNHEADERS if c[0] not in UNUSED_HEADERS) + FILE_HEADERS class FileRevModel(HgRepoListModel): """ Model used to manage the list of revisions of a file, in file viewer of in diff-file viewer dialogs. """ _allcolumns = tuple(h[0] for h in FILE_COLUMNHEADERS) _allcolnames = dict(FILE_COLUMNHEADERS) _columns = ('Graph', 'Rev', 'Branch', 'Description', 'Author', 'Age', 'Filename') _stretchs = {'Description': 1, } _getcolumns = "getFilelogColumns" def __init__(self, repo, cfgname, filename=None, parent=None): """ data is a HgHLRepo instance """ HgRepoListModel.__init__(self, repo, cfgname, '', [], False, parent) self.setFilename(filename) def setRepo(self, repo, branch='', fromhead=None, follow=False): self.repo = repo self._datacache = {} self.reloadConfig() def setFilename(self, filename): self.filename = filename self._user_colors = {} self._branch_colors = {} self.rowcount = 0 self._datacache = {} if self.filename: grapher = filelog_grapher(self.repo, self.filename) self.graph = Graph(self.repo, grapher) fl = self.repo.file(self.filename) # we use fl.index here (instead of linkrev) cause # linkrev API changed between 1.0 and 1.?. So this # works with both versions. self.heads = [fl.index[fl.rev(x)][4] for x in fl.heads()] self.ensureBuilt(row=self.fill_step/2) QTimer.singleShot(0, self, SIGNAL('filled()')) else: self.graph = None self.heads = [] def indexLinkedFromRev(self, rev): """Index for the last changed revision before the specified revision This does not follow renames. """ # as of Mercurial 2.6, workingfilectx.linkrev() does not work, and # this model has no virtual working-dir revision. if rev is None: rev = '.' try: fctx = self.repo[rev][self.filename] except error.LookupError: return None return self.indexFromRev(fctx.linkrev()) tortoisehg-2.10/tortoisehg/hgqt/run.py0000644000076400007640000011506512231647662017222 0ustar stevesteve# run.py - front-end script for TortoiseHg dialogs # # Copyright 2008 Steve Borho # Copyright 2008 TK Soh # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. shortlicense = ''' Copyright (C) 2008-2013 Steve Borho and others. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ''' import os import pdb import sys import subprocess import mercurial.ui as uimod from mercurial import util, fancyopts, cmdutil, extensions, error, scmutil from tortoisehg.hgqt.i18n import agettext as _ from tortoisehg.util import hglib, paths, i18n from tortoisehg.util import version as thgversion from tortoisehg.hgqt import qtapp, qtlib, thgrepo from tortoisehg.hgqt import quickop try: from tortoisehg.util.config import nofork as config_nofork except ImportError: config_nofork = None console_commands = 'help thgstatus version' nonrepo_commands = '''userconfig shellconfig clone init debugbugreport about help version thgstatus serve rejects log''' def dispatch(args, u=None): """run the command specified in args""" try: if u is None: u = uimod.ui() if '--traceback' in args: u.setconfig('ui', 'traceback', 'on') if '--debugger' in args: pdb.set_trace() if 'THGDEBUG' in os.environ: u.setconfig('ui', 'debug', 'on') return _runcatch(u, args) except error.ParseError, e: qtapp.earlyExceptionMsgBox(e) except SystemExit, e: return e.code except Exception, e: if '--debugger' in args: pdb.post_mortem(sys.exc_info()[2]) qtapp.earlyBugReport(e) return -1 except KeyboardInterrupt: print _('\nCaught keyboard interrupt, aborting.\n') return -1 origwdir = os.getcwd() def portable_fork(ui, opts): if 'THG_GUI_SPAWN' in os.environ or ( not opts.get('fork') and opts.get('nofork')): os.environ['THG_GUI_SPAWN'] = '1' return elif ui.configbool('tortoisehg', 'guifork', None) is not None: if not ui.configbool('tortoisehg', 'guifork'): return elif config_nofork: return portable_start_fork() sys.exit(0) def portable_start_fork(extraargs=None): os.environ['THG_GUI_SPAWN'] = '1' # Spawn background process and exit if hasattr(sys, "frozen"): args = sys.argv else: args = [sys.executable] + sys.argv if extraargs: args += extraargs cmdline = subprocess.list2cmdline(args) os.chdir(origwdir) subprocess.Popen(cmdline, creationflags=qtlib.openflags, shell=True) # Windows and Nautilus shellext execute # "thg subcmd --listfile TMPFILE" or "thg subcmd --listfileutf8 TMPFILE"(planning) . # Extensions written in .hg/hgrc is enabled after calling # extensions.loadall(lui) # # 1. win32mbcs extension # Japanese shift_jis and Chinese big5 include '0x5c'(backslash) in filename. # Mercurial resolves this problem with win32mbcs extension. # So, thg must parse path after loading win32mbcs extension. # # 2. fixutf8 extension # fixutf8 extension requires paths encoding utf-8. # So, thg need to convert to utf-8. # _lines = [] _linesutf8 = [] def get_lines_from_listfile(filename, isutf8): global _lines global _linesutf8 try: if filename == '-': lines = [ x.replace("\n", "") for x in sys.stdin.readlines() ] else: fd = open(filename, "r") lines = [ x.replace("\n", "") for x in fd.readlines() ] fd.close() os.unlink(filename) if isutf8: _linesutf8 = lines else: _lines = lines except IOError: sys.stderr.write(_('can not read file "%s". Ignored.\n') % filename) def get_files_from_listfile(): global _lines global _linesutf8 lines = [] need_to_utf8 = False if os.name == 'nt': try: fixutf8 = extensions.find("fixutf8") if fixutf8: need_to_utf8 = True except KeyError: pass if need_to_utf8: lines += _linesutf8 for l in _lines: lines.append(hglib.toutf(l)) else: lines += _lines for l in _linesutf8: lines.append(hglib.fromutf(l)) # Convert absolute file paths to repo/cwd canonical cwd = os.getcwd() root = paths.find_root(cwd) if not root: return lines if cwd == root: cwd_rel = '' else: cwd_rel = cwd[len(root+os.sep):] + os.sep files = [] for f in lines: try: cpath = scmutil.canonpath(root, cwd, f) # canonpath will abort on .hg/ paths except util.Abort: continue if cpath.startswith(cwd_rel): cpath = cpath[len(cwd_rel):] files.append(cpath) else: files.append(f) return files def _parse(ui, args): options = {} cmdoptions = {} try: args = fancyopts.fancyopts(args, globalopts, options) except fancyopts.getopt.GetoptError, inst: raise error.CommandError(None, inst) if args: alias, args = args[0], args[1:] elif options['help']: help_(ui, None) sys.exit() else: alias, args = 'workbench', [] aliases, i = cmdutil.findcmd(alias, table, ui.config("ui", "strict")) for a in aliases: if a.startswith(alias): alias = a break cmd = aliases[0] c = list(i[1]) # combine global options into local for o in globalopts: c.append((o[0], o[1], options[o[1]], o[3])) try: args = fancyopts.fancyopts(args, c, cmdoptions, True) except fancyopts.getopt.GetoptError, inst: raise error.CommandError(cmd, inst) # separate global options back out for o in globalopts: n = o[1] options[n] = cmdoptions[n] del cmdoptions[n] listfile = options.get('listfile') if listfile: del options['listfile'] get_lines_from_listfile(listfile, False) listfileutf8 = options.get('listfileutf8') if listfileutf8: del options['listfileutf8'] get_lines_from_listfile(listfileutf8, True) return (cmd, cmd and i[0] or None, args, options, cmdoptions, alias) def _runcatch(ui, args): try: try: return runcommand(ui, args) finally: ui.flush() except error.AmbiguousCommand, inst: ui.warn(_("thg: command '%s' is ambiguous:\n %s\n") % (inst.args[0], " ".join(inst.args[1]))) except error.UnknownCommand, inst: ui.warn(_("thg: unknown command '%s'\n") % inst.args[0]) help_(ui, 'shortlist') except error.CommandError, inst: if inst.args[0]: ui.warn(_("thg %s: %s\n") % (inst.args[0], inst.args[1])) help_(ui, inst.args[0]) else: ui.warn(_("thg: %s\n") % inst.args[1]) help_(ui, 'shortlist') except error.RepoError, inst: ui.warn(_("abort: %s!\n") % inst) return -1 def runcommand(ui, args): cmd, func, args, options, cmdoptions, alias = _parse(ui, args) cmdoptions['alias'] = alias ui.setconfig("ui", "verbose", str(bool(options["verbose"]))) i18n.setlanguage(ui.config('tortoisehg', 'ui.language')) if options['help']: return help_(ui, cmd) if options['newworkbench']: cmdoptions['newworkbench'] = True path = options['repository'] if path: if path.startswith('bundle:'): s = path[7:].split('+', 1) if len(s) == 1: path, bundle = os.getcwd(), s[0] else: path, bundle = s cmdoptions['bundle'] = os.path.abspath(bundle) path = ui.expandpath(path) # TODO: replace by abspath() if chdir() isn't necessary try: os.chdir(path) path = os.getcwd() except OSError: pass if options['profile']: options['nofork'] = True path = paths.find_root(path) if path: try: lui = ui.copy() lui.readconfig(os.path.join(path, ".hg", "hgrc")) except IOError: pass else: lui = ui hglib.wrapextensionsloader() # enable blacklist of extensions extensions.loadall(lui) args += get_files_from_listfile() if options['quiet']: ui.quiet = True # repository existence will be tested in qtrun() if cmd not in nonrepo_commands.split(): cmdoptions['repository'] = path or options['repository'] or '.' cmdoptions['mainapp'] = True checkedfunc = util.checksignature(func) if cmd in console_commands.split(): d = lambda: checkedfunc(ui, *args, **cmdoptions) else: portable_fork(ui, options) d = lambda: qtrun(checkedfunc, ui, *args, **cmdoptions) return _runcommand(lui, options, cmd, d) def _runcommand(ui, options, cmd, cmdfunc): def checkargs(): try: return cmdfunc() except error.SignatureError: raise error.CommandError(cmd, _("invalid arguments")) if options['profile']: format = ui.config('profiling', 'format', default='text') if not format in ['text', 'kcachegrind']: ui.warn(_("unrecognized profiling format '%s'" " - Ignored\n") % format) format = 'text' output = ui.config('profiling', 'output') if output: path = ui.expandpath(output) ostream = open(path, 'wb') else: ostream = sys.stderr try: from mercurial import lsprof except ImportError: raise util.Abort(_( 'lsprof not available - install from ' 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/')) p = lsprof.Profiler() p.enable(subcalls=True) try: return checkargs() finally: p.disable() if format == 'kcachegrind': import lsprofcalltree calltree = lsprofcalltree.KCacheGrind(p) calltree.output(ostream) else: # format == 'text' stats = lsprof.Stats(p.getstats()) stats.sort() stats.pprint(top=10, file=ostream, climit=5) if output: ostream.close() else: return checkargs() qtrun = qtapp.QtRunner() table = {} command = cmdutil.command(table) # common command options globalopts = [ ('R', 'repository', '', _('repository root directory or symbolic path name')), ('v', 'verbose', None, _('enable additional output')), ('q', 'quiet', None, _('suppress output')), ('h', 'help', None, _('display help and exit')), ('', 'debugger', None, _('start debugger')), ('', 'profile', None, _('print command execution profile')), ('', 'nofork', None, _('do not fork GUI process')), ('', 'fork', None, _('always fork GUI process')), ('', 'listfile', '', _('read file list from file')), ('', 'listfileutf8', '', _('read file list from file encoding utf-8')), ('', 'newworkbench', None, _('open a new workbench window')), ] # common command functions def _filelog(ui, repoagent, *pats, **opts): from tortoisehg.hgqt import filedialogs if len(pats) != 1: raise util.Abort(_('requires a single filename')) filename = hglib.canonpaths(pats)[0] return filedialogs.FileLogDialog(repoagent, filename) def _formatfilerevset(pats): q = [] for f in pats: pat = hglib.canonpaths([f])[0] if os.path.isdir(f): q.append('file("%s/**")' % pat) elif os.path.isfile(f): q.append('file("%s")' % pat) return ' or '.join(q) def _workbench(ui, *pats, **opts): root = opts.get('root') or paths.find_root() # TODO: unclear that _workbench() is called inside qtrun(). maybe this # function should receive factory object instead of using global qtrun. w = qtrun.createWorkbench() if root: root = hglib.tounicode(root) bundle = opts.get('bundle') if bundle: w.openRepo(root, False, bundle=hglib.tounicode(bundle)) else: w.showRepo(root) if pats: q = _formatfilerevset(pats) w.setRevsetFilter(root, hglib.tounicode(q)) if w.repoTabsWidget.count() <= 0: w.reporegistry.setVisible(True) return w # commands start here, listed alphabetically @command('about', [], _('thg about')) def about(ui, *pats, **opts): """about dialog""" from tortoisehg.hgqt import about as aboutmod return aboutmod.AboutDialog() @command('add', [], _('thg add [FILE]...')) def add(ui, repoagent, *pats, **opts): """add files""" return quickop.run(ui, repoagent, *pats, **opts) @command('^annotate|blame', [('r', 'rev', '', _('revision to annotate')), ('n', 'line', '', _('open to line')), ('p', 'pattern', '', _('initial search pattern'))], _('thg annotate')) def annotate(ui, repoagent, *pats, **opts): """annotate dialog""" from tortoisehg.hgqt import fileview repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, opts.get('rev')).rev() dlg = _filelog(ui, repoagent, *pats, **opts) dlg.setFileViewMode(fileview.AnnMode) dlg.goto(rev) if opts.get('line'): try: lineno = int(opts['line']) except ValueError: raise util.Abort(_('invalid line number: %s') % opts['line']) dlg.showLine(lineno) if opts.get('pattern'): dlg.setSearchPattern(hglib.tounicode(opts['pattern'])) return dlg @command('archive', [('r', 'rev', '', _('revision to archive'))], _('thg archive')) def archive(ui, repoagent, *pats, **opts): """archive dialog""" from tortoisehg.hgqt import archive as archivemod rev = opts.get('rev') return archivemod.ArchiveDialog(repoagent, rev) @command('^backout', [('', 'merge', None, _('merge with old dirstate parent after backout')), ('', 'parent', '', _('parent to choose when backing out merge')), ('r', 'rev', '', _('revision to backout'))], _('thg backout [OPTION]... [[-r] REV]')) def backout(ui, repoagent, *pats, **opts): """backout tool""" from tortoisehg.hgqt import backout as backoutmod if opts.get('rev'): rev = opts.get('rev') elif len(pats) == 1: rev = pats[0] else: rev = 'tip' return backoutmod.BackoutDialog(repoagent, rev) @command('^bisect', [], _('thg bisect')) def bisect(ui, repoagent, *pats, **opts): """bisect dialog""" from tortoisehg.hgqt import bisect as bisectmod return bisectmod.BisectDialog(repoagent, opts) @command('bookmarks|bookmark', [('r', 'rev', '', _('revision'))], _('thg bookmarks [-r REV] [NAME]')) def bookmark(ui, repoagent, *names, **opts): """add or remove a movable marker""" from tortoisehg.hgqt import bookmark as bookmarkmod repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, opts.get('rev')).rev() if len(names) > 1: raise util.Abort(_('only one new bookmark name allowed')) dlg = bookmarkmod.BookmarkDialog(repoagent, rev) if names: dlg.setBookmarkName(hglib.tounicode(names[0])) return dlg @command('^clone', [('U', 'noupdate', None, _('the clone will include an empty working copy ' '(only a repository)')), ('u', 'updaterev', '', _('revision, tag or branch to check out')), ('r', 'rev', '', _('include the specified changeset')), ('b', 'branch', [], _('clone only the specified branch')), ('', 'pull', None, _('use pull protocol to copy metadata')), ('', 'uncompressed', None, _('use uncompressed transfer ' '(fast over LAN)'))], _('thg clone [OPTION]... SOURCE [DEST]')) def clone(ui, *pats, **opts): """clone tool""" from tortoisehg.hgqt import clone as clonemod return clonemod.CloneDialog(pats, opts) @command('^commit|ci', [('u', 'user', '', _('record user as committer')), ('d', 'date', '', _('record datecode as commit date'))], _('thg commit [OPTIONS] [FILE]...')) def commit(ui, repoagent, *pats, **opts): """commit tool""" from tortoisehg.hgqt import commit as commitmod repo = repoagent.rawRepo() pats = hglib.canonpaths(pats) os.chdir(repo.root) return commitmod.CommitDialog(repoagent, pats, opts) @command('debugbugreport', [], _('thg debugbugreport [TEXT]')) def debugbugreport(ui, *pats, **opts): """open bugreport dialog by exception""" raise Exception(' '.join(pats)) @command('drag_copy', [], _('thg drag_copy SOURCE... DEST')) def drag_copy(ui, repoagent, *pats, **opts): """copy the selected files to the desired directory""" opts.update(alias='copy', headless=True) return quickop.run(ui, repoagent, *pats, **opts) @command('drag_move', [], _('thg drag_move SOURCE... DEST')) def drag_move(ui, repoagent, *pats, **opts): """move the selected files to the desired directory""" opts.update(alias='move', headless=True) return quickop.run(ui, repoagent, *pats, **opts) @command('^email', [('r', 'rev', [], _('a revision to send'))], _('thg email [REVS]')) def email(ui, repoagent, *revs, **opts): """send changesets by email""" from tortoisehg.hgqt import hgemail # TODO: same options as patchbomb if opts.get('rev'): if revs: raise util.Abort(_('use only one form to specify the revision')) revs = opts.get('rev') repo = repoagent.rawRepo() revs = scmutil.revrange(repo, revs) return hgemail.EmailDialog(repoagent, revs) @command('forget', [], _('thg forget [FILE]...')) def forget(ui, repoagent, *pats, **opts): """forget selected files""" return quickop.run(ui, repoagent, *pats, **opts) @command('graft', [('r', 'rev', [], _('revisions to graft'))], _('thg graft [-r] REV...')) def graft(ui, repoagent, *revs, **opts): """graft dialog""" from tortoisehg.hgqt import graft as graftmod repo = repoagent.rawRepo() revs = list(revs) revs.extend(opts['rev']) if not os.path.exists(repo.join('graftstate')) and not revs: raise util.Abort(_('You must provide revisions to graft')) return graftmod.GraftDialog(repoagent, None, source=revs) @command('^grep|search', [('i', 'ignorecase', False, _('ignore case during search'))], _('thg grep')) def grep(ui, repoagent, *pats, **opts): """grep/search dialog""" from tortoisehg.hgqt import grep as grepmod upats = [hglib.tounicode(p) for p in pats] return grepmod.SearchDialog(repoagent, upats, **opts) @command('^guess', [], _('thg guess')) def guess(ui, repoagent, *pats, **opts): """guess previous renames or copies""" from tortoisehg.hgqt import guess as guessmod return guessmod.DetectRenameDialog(repoagent, None, *pats) ### help management, adapted from mercurial.commands.help_() @command('help', [], _('thg help [COMMAND]')) def help_(ui, name=None, with_version=False, **opts): """show help for a command, extension, or list of commands With no arguments, print a list of commands and short help. Given a command name, print help for that command. Given an extension name, print help for that extension, and the commands it provides.""" option_lists = [] textwidth = ui.termwidth() - 2 def addglobalopts(aliases): if ui.verbose: option_lists.append((_("global options:"), globalopts)) if name == 'shortlist': option_lists.append((_('use "thg help" for the full list ' 'of commands'), ())) else: if name == 'shortlist': msg = _('use "thg help" for the full list of commands ' 'or "thg -v" for details') elif aliases: msg = _('use "thg -v help%s" to show aliases and ' 'global options') % (name and " " + name or "") else: msg = _('use "thg -v help %s" to show global options') % name option_lists.append((msg, ())) def helpcmd(name): if with_version: version(ui) ui.write('\n') try: aliases, i = cmdutil.findcmd(name, table, False) except error.AmbiguousCommand, inst: select = lambda c: c.lstrip('^').startswith(inst.args[0]) helplist(_('list of commands:\n\n'), select) return # synopsis ui.write("%s\n" % i[2]) # aliases if not ui.quiet and len(aliases) > 1: ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:])) # description doc = i[0].__doc__ if not doc: doc = _("(no help text available)") if ui.quiet: doc = doc.splitlines(0)[0] ui.write("\n%s\n" % doc.rstrip()) if not ui.quiet: # options if i[1]: option_lists.append((_("options:\n"), i[1])) addglobalopts(False) def helplist(header, select=None): h = {} cmds = {} for c, e in table.iteritems(): f = c.split("|", 1)[0] if select and not select(f): continue if (not select and name != 'shortlist' and e[0].__module__ != __name__): continue if name == "shortlist" and not f.startswith("^"): continue f = f.lstrip("^") if not ui.debugflag and f.startswith("debug"): continue doc = e[0].__doc__ if doc and 'DEPRECATED' in doc and not ui.verbose: continue #doc = gettext(doc) if not doc: doc = _("(no help text available)") h[f] = doc.splitlines()[0].rstrip() cmds[f] = c.lstrip("^") if not h: ui.status(_('no commands defined\n')) return ui.status(header) fns = sorted(h) m = max(map(len, fns)) for f in fns: if ui.verbose: commands = cmds[f].replace("|",", ") ui.write(" %s:\n %s\n"%(commands, h[f])) else: ui.write('%s\n' % (util.wrap(h[f], textwidth, initindent=' %-*s ' % (m, f), hangindent=' ' * (m + 4)))) if not ui.quiet: addglobalopts(True) def helptopic(name): from mercurial import help for names, header, doc in help.helptable: if name in names: break else: raise error.UnknownCommand(name) # description if not doc: doc = _("(no help text available)") if hasattr(doc, '__call__'): doc = doc() ui.write("%s\n" % header) ui.write("%s\n" % doc.rstrip()) if name and name != 'shortlist': i = None for f in (helpcmd, helptopic): try: f(name) i = None break except error.UnknownCommand, inst: i = inst if i: raise i else: # program name if ui.verbose or with_version: version(ui) else: ui.status(_("Thg - TortoiseHg's GUI tools for Mercurial SCM (Hg)\n")) ui.status('\n') # list of commands if name == "shortlist": header = _('basic commands:\n\n') else: header = _('list of commands:\n\n') helplist(header) # list all option lists opt_output = [] for title, options in option_lists: opt_output.append(("\n%s" % title, None)) for shortopt, longopt, default, desc in options: if "DEPRECATED" in desc and not ui.verbose: continue opt_output.append(("%2s%s" % (shortopt and "-%s" % shortopt, longopt and " --%s" % longopt), "%s%s" % (desc, default and _(" (default: %s)") % default or ""))) if opt_output: opts_len = max([len(line[0]) for line in opt_output if line[1]] or [0]) for first, second in opt_output: if second: initindent = ' %-*s ' % (opts_len, first) hangindent = ' ' * (opts_len + 3) ui.write('%s\n' % (util.wrap(second, textwidth, initindent=initindent, hangindent=hangindent))) else: ui.write("%s\n" % first) @command('^hgignore|ignore|filter', [], _('thg hgignore [FILE]')) def hgignore(ui, repoagent, *pats, **opts): """ignore filter editor""" from tortoisehg.hgqt import hgignore as hgignoremod if pats and pats[0].endswith('.hgignore'): pats = [] return hgignoremod.HgignoreDialog(repoagent, None, *pats) @command('import', [('', 'mq', False, _('import to the patch queue (MQ)'))], _('thg import [OPTION] [SOURCE]...')) def import_(ui, repoagent, *pats, **opts): """import an ordered set of patches""" from tortoisehg.hgqt import thgimport dlg = thgimport.ImportDialog(repoagent, None, **opts) dlg.setfilepaths(pats) return dlg @command('^init', [], _('thg init [DEST]')) def init(ui, *pats, **opts): """init dialog""" from tortoisehg.hgqt import hginit return hginit.InitDialog(pats, opts) @command('^log|history|explorer|workbench', [('l', 'limit', '', _('(DEPRECATED)'))], _('thg log [OPTIONS] [FILE]')) def log(ui, *pats, **opts): """workbench application""" root = opts.get('root') or paths.find_root() if root and len(pats) == 1 and os.path.isfile(pats[0]): # TODO: do not instantiate repo here repo = thgrepo.repository(ui, root) repoagent = repo._pyqtobj return _filelog(ui, repoagent, *pats, **opts) # Before starting the workbench, we must check if we must try to reuse an # existing workbench window (we don't by default) # Note that if the "single workbench mode" is enabled, and there is no # existing workbench window, we must tell the Workbench object to create # the workbench server singleworkbenchmode = ui.configbool('tortoisehg', 'workbench.single', True) mustcreateserver = False if singleworkbenchmode: newworkbench = opts.get('newworkbench') if root and not newworkbench: if qtapp.connectToExistingWorkbench(root, _formatfilerevset(pats)): # The were able to connect to an existing workbench server, and # it confirmed that it has opened the selected repo for us sys.exit(0) # there is no pre-existing workbench server serverexists = False else: serverexists = qtapp.connectToExistingWorkbench('[echo]') # When in " single workbench mode", we must create a server if there # is not one already mustcreateserver = not serverexists w = _workbench(ui, *pats, **opts) if mustcreateserver: qtrun.createWorkbenchServer() return w @command('manifest', [('r', 'rev', '', _('revision to display')), ('n', 'line', '', _('open to line')), ('p', 'pattern', '', _('initial search pattern'))], _('thg manifest [-r REV] [FILE]')) def manifest(ui, repoagent, *pats, **opts): """display the current or given revision of the project manifest""" from tortoisehg.hgqt import manifestdialog repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, opts.get('rev')).rev() dlg = manifestdialog.ManifestDialog(repoagent, rev) if pats: path = hglib.canonpaths(pats)[0] if opts.get('line'): try: lineno = int(opts['line']) except ValueError: raise util.Abort(_('invalid line number: %s') % opts['line']) else: lineno = None dlg.setSource(hglib.tounicode(path), rev, lineno) if opts.get('pattern'): dlg.setSearchPattern(hglib.tounicode(opts['pattern'])) return dlg @command('^merge', [('r', 'rev', '', _('revision to merge'))], _('thg merge [[-r] REV]')) def merge(ui, repoagent, *pats, **opts): """merge wizard""" from tortoisehg.hgqt import merge as mergemod rev = opts.get('rev') or None if not rev and len(pats): rev = pats[0] if not rev: raise util.Abort(_('Merge revision not specified or not found')) return mergemod.MergeDialog(repoagent, rev) @command('postreview', [('r', 'rev', [], _('a revision to post'))], _('thg postreview [-r] REV...')) def postreview(ui, repoagent, *pats, **opts): """post changesets to reviewboard""" from tortoisehg.hgqt import postreview as postreviewmod repo = repoagent.rawRepo() revs = opts.get('rev') or None if not revs and len(pats): revs = pats[0] if not revs: raise util.Abort(_('no revisions specified')) return postreviewmod.PostReviewDialog(repo.ui, repoagent, revs) @command('^purge', [], _('thg purge')) def purge(ui, repoagent, *pats, **opts): """purge unknown and/or ignore files from repository""" from tortoisehg.hgqt import purge as purgemod return purgemod.PurgeDialog(repoagent) @command('^rebase', [('', 'keep', False, _('keep original changesets')), ('', 'keepbranches', False, _('keep original branch names')), ('', 'detach', False, _('(DEPRECATED)')), ('s', 'source', '', _('rebase from the specified changeset')), ('d', 'dest', '', _('rebase onto the specified changeset'))], _('thg rebase -s REV -d REV [--keep]')) def rebase(ui, repoagent, *pats, **opts): """rebase dialog""" from tortoisehg.hgqt import rebase as rebasemod repo = repoagent.rawRepo() if os.path.exists(repo.join('rebasestate')): # TODO: move info dialog into RebaseDialog if possible qtlib.InfoMsgBox(hglib.tounicode(_('Rebase already in progress')), hglib.tounicode(_('Resuming rebase already in ' 'progress'))) elif not opts['source'] or not opts['dest']: raise util.Abort(_('You must provide source and dest arguments')) return rebasemod.RebaseDialog(repoagent, None, **opts) @command('rejects', [], _('thg rejects [FILE]')) def rejects(ui, *pats, **opts): """manually resolve rejected patch chunks""" from tortoisehg.hgqt import rejects as rejectsmod if len(pats) != 1: raise util.Abort(_('You must provide the path to a file')) path = pats[0] if path.endswith('.rej'): path = path[:-4] return rejectsmod.RejectsDialog(ui, path) @command('remove|rm', [], _('thg remove [FILE]...')) def remove(ui, repoagent, *pats, **opts): """remove selected files""" return quickop.run(ui, repoagent, *pats, **opts) @command('rename|mv|copy', [], _('thg rename SOURCE [DEST]...')) def rename(ui, repoagent, *pats, **opts): """rename dialog""" from tortoisehg.hgqt import rename as renamemod iscopy = (opts.get('alias') == 'copy') return renamemod.RenameDialog(repoagent, pats, iscopy=iscopy) @command('^repoconfig', [('', 'focus', '', _('field to give initial focus'))], _('thg repoconfig')) def repoconfig(ui, repoagent, *pats, **opts): """repository configuration editor""" from tortoisehg.hgqt import settings return settings.SettingsDialog(True, focus=opts.get('focus')) @command('resolve', [], _('thg resolve')) def resolve(ui, repoagent, *pats, **opts): """resolve dialog""" from tortoisehg.hgqt import resolve as resolvemod return resolvemod.ResolveDialog(repoagent) @command('^revdetails', [('r', 'rev', '', _('the revision to show'))], _('thg revdetails [-r REV]')) def revdetails(ui, repoagent, *pats, **opts): """revision details tool""" from tortoisehg.hgqt import revdetails as revdetailsmod repo = repoagent.rawRepo() os.chdir(repo.root) rev = opts.get('rev', '.') return revdetailsmod.RevDetailsDialog(repoagent, rev=rev) @command('revert', [], _('thg revert [FILE]...')) def revert(ui, repoagent, *pats, **opts): """revert selected files""" return quickop.run(ui, repoagent, *pats, **opts) @command('rupdate', [('r', 'rev', '', _('revision to update'))], _('thg rupdate [[-r] REV]')) def rupdate(ui, repoagent, *pats, **opts): """update a remote repository""" from tortoisehg.hgqt import rupdate as rupdatemod rev = None if opts.get('rev'): rev = opts.get('rev') elif len(pats) == 1: rev = pats[0] return rupdatemod.rUpdateDialog(repoagent, rev, None, opts) @command('^serve', [('', 'web-conf', '', _('name of the hgweb config file (serve more than ' 'one repository)')), ('', 'webdir-conf', '', _('name of the hgweb config file (DEPRECATED)'))], _('thg serve [--web-conf FILE]')) def serve(ui, *pats, **opts): """start stand-alone webserver""" from tortoisehg.hgqt import serve as servemod return servemod.run(ui, *pats, **opts) if os.name == 'nt': # TODO: extra detection to determine if shell extension is installed @command('shellconfig', [], _('thg shellconfig')) def shellconfig(ui, *pats, **opts): """explorer extension configuration editor""" from tortoisehg.hgqt import shellconf return shellconf.ShellConfigWindow() @command('shelve|unshelve', [], _('thg shelve')) def shelve(ui, repoagent, *pats, **opts): """move changes between working directory and patches""" from tortoisehg.hgqt import shelve as shelvemod return shelvemod.ShelveDialog(repoagent) @command('^sign', [('f', 'force', None, _('sign even if the sigfile is modified')), ('l', 'local', None, _('make the signature local')), ('k', 'key', '', _('the key id to sign with')), ('', 'no-commit', None, _('do not commit the sigfile after signing')), ('m', 'message', '', _('use as commit message'))], _('thg sign [-f] [-l] [-k KEY] [-m TEXT] [REV]')) def sign(ui, repoagent, *pats, **opts): """sign tool""" from tortoisehg.hgqt import sign as signmod repo = repoagent.rawRepo() if 'gpg' not in repo.extensions(): raise util.Abort(_('Please enable the Gpg extension first.')) kargs = {} rev = len(pats) > 0 and pats[0] or None if rev: kargs['rev'] = rev return signmod.SignDialog(repoagent, opts=opts, **kargs) @command('^status|st', [('c', 'clean', False, _('show files without changes')), ('i', 'ignored', False, _('show ignored files'))], _('thg status [OPTIONS] [FILE]')) def status(ui, repoagent, *pats, **opts): """browse working copy status""" from tortoisehg.hgqt import status as statusmod repo = repoagent.rawRepo() pats = hglib.canonpaths(pats) os.chdir(repo.root) return statusmod.StatusDialog(repoagent, pats, opts) @command('^strip', [('f', 'force', None, _('discard uncommitted changes (no backup)')), ('n', 'nobackup', None, _('do not back up stripped revisions')), ('r', 'rev', '', _('revision to strip'))], _('thg strip [-f] [-n] [[-r] REV]')) def strip(ui, repoagent, *pats, **opts): """strip dialog""" from tortoisehg.hgqt import thgstrip rev = None if opts.get('rev'): rev = opts.get('rev') elif len(pats) == 1: rev = pats[0] return thgstrip.StripDialog(repoagent, rev=rev, opts=opts) @command('^sync|synchronize', [], _('thg sync [PEER]')) def sync(ui, repoagent, *pats, **opts): """synchronize with other repositories""" from tortoisehg.hgqt import sync as syncmod w = syncmod.SyncWidget(repoagent, None, **opts) if pats: w.setUrl(hglib.tounicode(pats[0])) return w @command('^tag', [('f', 'force', None, _('replace existing tag')), ('l', 'local', None, _('make the tag local')), ('r', 'rev', '', _('revision to tag')), ('', 'remove', None, _('remove a tag')), ('m', 'message', '', _('use as commit message'))], _('thg tag [-f] [-l] [-m TEXT] [-r REV] [NAME]')) def tag(ui, repoagent, *pats, **opts): """tag tool""" from tortoisehg.hgqt import tag as tagmod kargs = {} tag = len(pats) > 0 and pats[0] or None if tag: kargs['tag'] = tag rev = opts.get('rev') if rev: kargs['rev'] = rev return tagmod.TagDialog(repoagent, opts=opts, **kargs) @command('thgstatus', [('', 'delay', None, _('wait until the second ticks over')), ('n', 'notify', [], _('notify the shell for paths given')), ('', 'remove', None, _('remove the status cache')), ('s', 'show', None, _('show the contents of the status cache ' '(no update)')), ('', 'all', None, _('udpate all repos in current dir'))], _('thg thgstatus [OPTION]')) def thgstatus(ui, *pats, **opts): """update TortoiseHg status cache""" from tortoisehg.util import thgstatus as thgstatusmod thgstatusmod.run(ui, *pats, **opts) @command('^update|checkout|co', [('C', 'clean', None, _('discard uncommitted changes (no backup)')), ('r', 'rev', '', _('revision to update')),], _('thg update [-C] [[-r] REV]')) def update(ui, repoagent, *pats, **opts): """update/checkout tool""" from tortoisehg.hgqt import update as updatemod rev = None if opts.get('rev'): rev = opts.get('rev') elif len(pats) == 1: rev = pats[0] return updatemod.UpdateDialog(repoagent, rev, None, opts) @command('^userconfig', [('', 'focus', '', _('field to give initial focus'))], _('thg userconfig')) def userconfig(ui, *pats, **opts): """user configuration editor""" from tortoisehg.hgqt import settings return settings.SettingsDialog(False, focus=opts.get('focus')) @command('^vdiff', [('c', 'change', '', _('changeset to view in diff tool')), ('r', 'rev', [], _('revisions to view in diff tool')), ('b', 'bundle', '', _('bundle file to preview'))], _('launch visual diff tool')) def vdiff(ui, repoagent, *pats, **opts): """launch configured visual diff tool""" from tortoisehg.hgqt import visdiff repo = repoagent.rawRepo() if opts.get('bundle'): repo = thgrepo.repository(ui, opts.get('bundle')) pats = hglib.canonpaths(pats) return visdiff.visualdiff(ui, repo, pats, opts) @command('^version', [('v', 'verbose', None, _('print license'))], _('thg version [OPTION]')) def version(ui, **opts): """output version and copyright information""" ui.write(_('TortoiseHg Dialogs (version %s), ' 'Mercurial (version %s)\n') % (thgversion.version(), hglib.hgversion)) if not ui.quiet: ui.write(shortlicense) tortoisehg-2.10/tortoisehg/hgqt/matching.py0000644000076400007640000002473512170335562020206 0ustar stevesteve# matching.py - Find similar (matching) revisions dialog for TortoiseHg # # Copyright 2012 Angel Ezquerra # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error, revset from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import csinfo, qtlib from PyQt4.QtCore import * from PyQt4.QtGui import * class MatchDialog(QDialog): revmatch = pyqtSignal(QString) def __init__(self, repo, rev=None, parent=None): super(MatchDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self.revsetexpression = '' self.repo = repo # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid self.grid = QGridLayout() self.grid.setSpacing(6) box.addLayout(self.grid) ### matched revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) self.grid.addWidget(QLabel(_('Find revisions matching fields of:')), 0, 0) self.grid.addWidget(combo, 0, 1) # Give the combo box a minimum width that will ensure that the dialog is # large enough to fit the additional progress bar that will appear when # updating subrepositories. combo.setMinimumWidth(450) if rev is None: rev = self.repo.dirstate.branch() else: rev = str(rev) combo.addItem(hglib.tounicode(rev)) combo.setCurrentIndex(0) # make it easy to match the workding directory parent revision combo.addItem(hglib.tounicode('.')) tags = list(self.repo.tags()) + repo._bookmarks.keys() tags.sort(reverse=True) for tag in tags: combo.addItem(hglib.tounicode(tag)) ### matched revision info self.rev_to_match_info_text = QLabel() self.rev_to_match_info_text.setShown(False) style = csinfo.panelstyle(contents=('cset', 'branch', 'close', 'user', 'dateage', 'parents', 'children', 'tags', 'graft', 'transplant', 'p4', 'svn', 'converted'), selectable=True, expandable=True) factory = csinfo.factory(self.repo, style=style) self.rev_to_match_info = factory() self.rev_to_match_info_lbl = QLabel(_('Revision to Match:')) self.grid.addWidget(self.rev_to_match_info_lbl, 1, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addWidget(self.rev_to_match_info, 1, 1) self.grid.addWidget(self.rev_to_match_info_text, 1, 1) ### fields that will be matched self.optbox = QVBoxLayout() self.optbox.setSpacing(6) expander = qtlib.ExpanderLabel(_('Fields to match:'), False) expander.expanded.connect(self.show_options) row = self.grid.rowCount() self.grid.addWidget(expander, row, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addLayout(self.optbox, row, 1) self.summary_chk = QCheckBox(_('Summary (first description line)')) self.description_chk = QCheckBox(_('Description')) self.desc_btngroup = QButtonGroup() self.desc_btngroup.setExclusive(False) self.desc_btngroup.addButton(self.summary_chk) self.desc_btngroup.addButton(self.description_chk) self.desc_btngroup.buttonClicked.connect(self._selectSummaryOrDescription) self.author_chk = QCheckBox(_('Author')) self.date_chk = QCheckBox(_('Date')) self.files_chk = QCheckBox(_('Files')) self.diff_chk = QCheckBox(_('Diff contents')) self.substate_chk = QCheckBox(_('Subrepo states')) self.branch_chk = QCheckBox(_('Branch')) self.parents_chk = QCheckBox(_('Parents')) self.phase_chk = QCheckBox(_('Phase')) self._hideable_chks = (self.branch_chk, self.phase_chk, self.parents_chk,) self.optbox.addWidget(self.summary_chk) self.optbox.addWidget(self.description_chk) self.optbox.addWidget(self.author_chk) self.optbox.addWidget(self.date_chk) self.optbox.addWidget(self.files_chk) self.optbox.addWidget(self.diff_chk) self.optbox.addWidget(self.substate_chk) self.optbox.addWidget(self.branch_chk) self.optbox.addWidget(self.parents_chk) self.optbox.addWidget(self.phase_chk) s = QSettings() #### Persisted Options self.summary_chk.setChecked(s.value('matching/summary', False).toBool()) self.description_chk.setChecked(s.value('matching/description', True).toBool()) self.author_chk.setChecked(s.value('matching/author', True).toBool()) self.branch_chk.setChecked(s.value('matching/branch', False).toBool()) self.date_chk.setChecked(s.value('matching/date', True).toBool()) self.files_chk.setChecked(s.value('matching/files', False).toBool()) self.diff_chk.setChecked(s.value('matching/diff', False).toBool()) self.parents_chk.setChecked(s.value('matching/parents', False).toBool()) self.phase_chk.setChecked(s.value('matching/phase', False).toBool()) self.substate_chk.setChecked(s.value('matching/substate', False).toBool()) ## bottom buttons buttons = QDialogButtonBox() self.close_btn = buttons.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.reject) self.close_btn.setAutoDefault(False) self.match_btn = buttons.addButton(_('&Match'), QDialogButtonBox.ActionRole) self.match_btn.clicked.connect(self.match) box.addWidget(buttons) # signal handlers self.rev_combo.editTextChanged.connect(self.update_info) # dialog setting self.setLayout(box) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.setWindowTitle(_('Find matches - %s') % self.repo.displayname) self.setWindowIcon(qtlib.geticon('hg-update')) # prepare to show self.update_info() if not self.match_btn.isEnabled(): self.rev_combo.lineEdit().selectAll() # need to change rev # expand options if a hidden one is checked hiddenOptionsChecked = self.hiddenSettingIsChecked() self.show_options(hiddenOptionsChecked) expander.set_expanded(hiddenOptionsChecked) ### Private Methods ### def hiddenSettingIsChecked(self): for chk in self._hideable_chks: if chk.isChecked(): return True return False def saveSettings(self): s = QSettings() s.setValue('matching/summary', self.summary_chk.isChecked()) s.setValue('matching/description', self.description_chk.isChecked()) s.setValue('matching/author', self.author_chk.isChecked()) s.setValue('matching/branch', self.branch_chk.isChecked()) s.setValue('matching/date', self.date_chk.isChecked()) s.setValue('matching/files', self.files_chk.isChecked()) s.setValue('matching/diff', self.diff_chk.isChecked()) s.setValue('matching/parents', self.parents_chk.isChecked()) s.setValue('matching/phase', self.phase_chk.isChecked()) s.setValue('matching/substate', self.substate_chk.isChecked()) def update_info(self, *args): def set_csinfo_mode(mode): """Show the csinfo widget or the info text label""" # hide first, then show if mode: self.rev_to_match_info_text.setShown(False) self.rev_to_match_info.setShown(True) else: self.rev_to_match_info.setShown(False) self.rev_to_match_info_text.setShown(True) def csinfo_update(ctx): self.rev_to_match_info.update(ctx) set_csinfo_mode(True) def csinfo_set_text(text): self.rev_to_match_info_text.setText(text) set_csinfo_mode(False) self.rev_to_match_info_lbl.setText(_('Revision to Match:')) new_rev = hglib.fromunicode(self.rev_combo.currentText()) if new_rev.lower() == 'null': self.match_btn.setEnabled(True) return try: csinfo_update(self.repo[new_rev]) return except (error.LookupError, error.RepoLookupError, error.RepoError): pass # If we get this far, assume we are matching a revision set validrevset = False try: func = revset.match(self.repo.ui, new_rev) rset = [c for c in func(self.repo, list(self.repo))] if len(rset) > 1: self.rev_to_match_info_lbl.setText(_('Revisions to Match:')) csinfo_set_text(_('Match any of %d revisions') \ % len(rset)) else: self.rev_to_match_info_lbl.setText(_('Revision to Match:')) csinfo_update(rset[0]) validrevset = True except (error.LookupError, error.RepoLookupError): csinfo_set_text(_('Unknown revision!')) except (error.ParseError): csinfo_set_text(_('Parse Error!')) if validrevset: self.match_btn.setEnabled(True) else: self.match_btn.setDisabled(True) def match(self): self.saveSettings() fieldmap = { 'summary': self.summary_chk, 'description': self.description_chk, 'author': self.author_chk, 'branch': self.branch_chk, 'date': self.date_chk, 'files': self.files_chk, 'diff': self.diff_chk, 'parents': self.parents_chk, 'phase': self.phase_chk, 'substate': self.substate_chk, } fields = [] for (field, chk) in fieldmap.items(): if chk.isChecked(): fields.append(field) rev = hglib.fromunicode(self.rev_combo.currentText()) if fields: self.revsetexpression = "matching(%s, '%s')" % (rev, ' '.join(fields)) else: self.revsetexpression = "matching(%s)" % (rev) self.accept() ### Signal Handlers ### def show_options(self, visible): for chk in self._hideable_chks: chk.setShown(visible) @pyqtSlot(QAbstractButton) def _selectSummaryOrDescription(self, btn): # Uncheck all other buttons for b in self.desc_btngroup.buttons(): if not (b is btn): b.setChecked(False) tortoisehg-2.10/tortoisehg/hgqt/revpanel.py0000644000076400007640000001657012235634453020231 0ustar stevesteve# revpanel.py - TortoiseHg rev panel widget # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright (C) 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from mercurial import error from tortoisehg.util import hglib, obsoleteutil from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import csinfo, qtlib from PyQt4.QtCore import * def label_func(widget, item, ctx): if item == 'cset': if type(ctx.rev()) is str: return _('Patch:') return _('Changeset:') elif item == 'parents': return _('Parent:') elif item == 'children': return _('Child:') elif item == 'precursors': return _('Precursors:') elif item == 'successors': return _('Successors:') raise csinfo.UnknownItem(item) def revid_markup(revid, **kargs): opts = dict(family='monospace', size='9pt') opts.update(kargs) return qtlib.markup(revid, **opts) def data_func(widget, item, ctx): def summary_line(desc): return hglib.longsummary(desc.replace('\0', '')) def revline_data(ctx, hl=False, branch=None): if isinstance(ctx, basestring): return ctx desc = ctx.description() return (str(ctx.rev()), str(ctx), summary_line(desc), hl, branch) def format_ctxlist(ctxlist): if not ctxlist: return None return [revline_data(ctx)[:3] for ctx in ctxlist] if item == 'cset': return revline_data(ctx) elif item == 'branch': value = hglib.tounicode(ctx.branch()) return value != 'default' and value or None elif item == 'parents': # TODO: need to put 'diff to other' checkbox #pindex = self.diff_other_parent() and 1 or 0 pindex = 0 # always show diff with first parent pctxs = ctx.parents() parents = [] for pctx in pctxs: highlight = len(pctxs) == 2 and pctx == pctxs[pindex] branch = None if hasattr(pctx, 'branch') and pctx.branch() != ctx.branch(): branch = pctx.branch() parents.append(revline_data(pctx, highlight, branch)) return parents elif item == 'children': children = [] for cctx in ctx.children(): branch = None if hasattr(cctx, 'branch') and cctx.branch() != ctx.branch(): branch = cctx.branch() children.append(revline_data(cctx, branch=branch)) return children elif item in ('graft', 'transplant', 'mqoriginalparent', 'p4', 'svn', 'converted',): ts = widget.get_data(item, usepreset=True) if not ts: return None try: tctx = ctx._repo[ts] return revline_data(tctx) except (error.LookupError, error.RepoLookupError, error.RepoError): return ts elif item == 'ishead': if ctx.rev() is None: ctx = ctx.p1() childbranches = [cctx.branch() for cctx in ctx.children()] return ctx.branch() not in childbranches elif item == 'isclose': if ctx.rev() is None: ctx = ctx.p1() return ctx.extra().get('close') is not None elif item == 'precursors': ctxlist = obsoleteutil.first_known_precursors(ctx) return format_ctxlist(ctxlist) elif item == 'successors': ctxlist = obsoleteutil.first_known_successors(ctx) return format_ctxlist(ctxlist) raise csinfo.UnknownItem(item) def create_markup_func(ui): def link_markup(revnum, revid, linkpattern=None): mrevid = revid_markup('%s (%s)' % (revnum, revid)) if linkpattern is None: return mrevid link = linkpattern.replace('{node|short}', revid).replace('{rev}', revnum) return '%s' % (link, mrevid) def revline_markup(revnum, revid, summary, highlight=None, branch=None, linkpattern='cset:{node|short}'): def branch_markup(branch): opts = dict(fg='black', bg='#aaffaa') return qtlib.markup(' %s ' % branch, **opts) summary = qtlib.markup(summary) if branch: branch = branch_markup(branch) if revid: rev = link_markup(revnum, revid, linkpattern=linkpattern) if branch: return '%s %s %s' % (rev, branch, summary) return '%s %s' % (rev, summary) else: revnum = qtlib.markup(revnum) if branch: return '%s - %s %s' % (revnum, branch, summary) return '%s - %s' % (revnum, summary) def markup_func(widget, item, value): if item in ('cset', 'graft', 'transplant', 'mqoriginalparent', 'p4', 'svn', 'converted'): if item == 'cset': linkpattern = ui.config('tortoisehg', 'changeset.link', None) else: linkpattern = 'cset:{node|short}' if isinstance(value, basestring): return revid_markup(value) return revline_markup(linkpattern=linkpattern, *value) elif item in ('parents', 'children', 'precursors', 'successors'): csets = [] for cset in value: if isinstance(cset, basestring): csets.append(revid_markup(cset)) else: csets.append(revline_markup(*cset)) return csets raise csinfo.UnknownItem(item) return markup_func def RevPanelWidget(repo): '''creates a rev panel widget and returns it''' custom = csinfo.custom(data=data_func, label=label_func, markup=create_markup_func(repo.ui)) style = csinfo.panelstyle(contents=('cset', 'branch', 'obsolete', 'close', 'user', 'dateage', 'parents', 'children', 'tags', 'graft', 'transplant', 'mqoriginalparent', 'precursors', 'successors', 'p4', 'svn', 'converted'), selectable=True, expandable=True) return csinfo.create(repo, style=style, custom=custom) def nomarkup(widget, item, value): def revline_markup(revnum, revid, summary, highlight=None, branch=None): summary = qtlib.markup(summary) if revid: rev = revid_markup('%s (%s)' % (revnum, revid)) return '%s %s' % (rev, summary) else: revnum = qtlib.markup(revnum) return '%s - %s' % (revnum, summary) csets = [] if item == 'ishead': if value is False: text = _('Not a head revision!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) elif item == 'isclose': if value is True: text = _('Head is closed!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) for cset in value: if isinstance(cset, basestring): csets.append(revid_markup(cset)) else: csets.append(revline_markup(*cset)) return csets def ParentWidget(repo): 'creates a parent rev widget and returns it' custom = csinfo.custom(data=data_func, label=label_func, markup=nomarkup) style = csinfo.panelstyle(contents=('parents', 'ishead', 'isclose'), selectable=True) return csinfo.create(repo, style=style, custom=custom) tortoisehg-2.10/tortoisehg/hgqt/hginit.py0000644000076400007640000001743712170335562017677 0ustar stevesteve# hginit.py - TortoiseHg dialog to initialize a repo # # Copyright 2008 Steve Borho # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import hg, ui, error, util from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.util import hglib, shlib from PyQt4.QtCore import * from PyQt4.QtGui import * class InitDialog(QDialog): """TortoiseHg init dialog""" def __init__(self, destdir=[], opts={}, parent=None): super(InitDialog, self).__init__(parent) # main layout self.vbox = QVBoxLayout() self.vbox.setSpacing(6) self.grid = QGridLayout() self.grid.setSpacing(6) self.vbox.addLayout(self.grid) # dest widgets self.dest_lbl = QLabel(_('Destination path:')) self.dest_edit = QLineEdit() self.dest_edit.setMinimumWidth(300) self.dest_btn = QPushButton(_('Browse...')) self.dest_btn.setAutoDefault(False) self.grid.addWidget(self.dest_lbl, 0, 0) self.grid.addWidget(self.dest_edit, 0, 1) self.grid.addWidget(self.dest_btn, 0, 2) # options checkboxes self.add_files_chk = QCheckBox( _('Add special files (.hgignore, ...)')) self.make_pre_1_7_chk = QCheckBox( _('Make repo compatible with Mercurial <1.7')) self.run_wb_chk = QCheckBox( _('Show in Workbench after init')) self.grid.addWidget(self.add_files_chk, 1, 1) self.grid.addWidget(self.make_pre_1_7_chk, 2, 1) if not self.parent(): self.grid.addWidget(self.run_wb_chk, 3, 1) # buttons self.init_btn = QPushButton(_('Create')) self.init_btn.setDefault(True) self.close_btn = QPushButton(_('&Close')) self.close_btn.setAutoDefault(False) self.hbox = QHBoxLayout() self.hbox.addStretch(0) self.hbox.addWidget(self.init_btn) self.hbox.addWidget(self.close_btn) self.vbox.addLayout(self.hbox) # some extras self.hgcmd_lbl = QLabel(_('Hg command:')) self.hgcmd_lbl.setAlignment(Qt.AlignRight) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) self.grid.addWidget(self.hgcmd_lbl, 4, 0) self.grid.addWidget(self.hgcmd_txt, 4, 1) # init defaults self.cwd = os.getcwd() path = os.path.abspath(destdir and destdir[0] or self.cwd) if os.path.isfile(path): path = os.path.dirname(path) self.dest_edit.setText(hglib.tounicode(path)) self.add_files_chk.setChecked(True) self.make_pre_1_7_chk.setChecked(False) self.compose_command() # dialog settings self.setWindowTitle(_('New Repository')) self.setWindowIcon(qtlib.geticon('hg-init')) self.setWindowFlags( self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setLayout(self.vbox) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.dest_edit.setFocus() # connecting slots self.dest_edit.textChanged.connect(self.compose_command) self.dest_btn.clicked.connect(self.browse_clicked) self.init_btn.clicked.connect(self.init) self.close_btn.clicked.connect(self.close) self.make_pre_1_7_chk.toggled.connect(self.compose_command) def browse_clicked(self): """Select the destination directory""" dest = hglib.fromunicode(self.dest_edit.text()) if not os.path.exists(dest): dest = os.path.dirname(dest) FD = QFileDialog caption = _('Select Destination Folder') path = FD.getExistingDirectory(parent=self, caption=caption, options=FD.ShowDirsOnly | FD.ReadOnly) if path: self.dest_edit.setText(path) def compose_command(self): # just a stub for extension with extra options (--mq, --ssh, ...) cmd = ['hg', 'init'] if self.make_pre_1_7_chk.isChecked(): cmd.append('--config format.dotencode=False') cmd.append(self.getPath()) self.hgcmd_txt.setText(hglib.tounicode(' '.join(cmd))) def getPath(self): return hglib.fromunicode(self.dest_edit.text()).strip() def init(self): dest = self.getPath() if dest == '': qtlib.ErrorMsgBox(_('Error executing init'), _('Destination path is empty'), _('Please enter the directory path')) self.dest_edit.setFocus() return False dest = os.path.normpath(dest) self.dest_edit.setText(hglib.tounicode(dest)) udest = self.dest_edit.text() if not os.path.exists(dest): p = dest l = 0 while not os.path.exists(p): l += 1 p, t = os.path.split(p) if not t: break # already root path if l > 1: res = qtlib.QuestionMsgBox(_('Init'), _('Are you sure about adding the new repository ' '%d extra levels deep?') % l, _('Path exists up to:\n%s\nand you asked for:\n%s') % (p, udest), defaultbutton=QMessageBox.No) if not res: self.dest_edit.setFocus() return try: # create the folder, just like Hg would os.makedirs(dest) except: qtlib.ErrorMsgBox(_('Error executing init'), _('Cannot create folder %s') % udest) return False _ui = ui.ui() # dotencode is the new default repo format in Mercurial 1.7 if self.make_pre_1_7_chk.isChecked(): _ui.setconfig('format', 'dotencode', 'False') try: # create the new repo hg.repository(_ui, dest, create=1) except error.RepoError, inst: qtlib.ErrorMsgBox(_('Error executing init'), _('Unable to create new repository'), hglib.tounicode(str(inst))) return False except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) qtlib.ErrorMsgBox(_('Error executing init'), _('Error when creating repository'), err) return False except: import traceback qtlib.ErrorMsgBox(_('Error executing init'), _('Error when creating repository'), traceback.format_exc()) return False # Create the .hg* file, mainly to workaround # Explorer's problem in creating files with a name # beginning with a dot. if (self.add_files_chk.isChecked() and os.path.exists(os.path.sep.join([dest, '.hg']))): hgignore = os.path.join(dest, '.hgignore') if not os.path.exists(hgignore): try: open(hgignore, 'wb') except: pass if self.run_wb_chk.isChecked(): # TODO: implement by using signal-slot if possible from tortoisehg.hgqt import run run.qtrun.showRepoInWorkbench(udest) else: if not self.parent(): qtlib.InfoMsgBox(_('Init'), _('

Repository successfully created at

%s

') % udest) self.accept() def reject(self): super(InitDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/cmdui.py0000644000076400007640000003616112231647662017516 0ustar stevesteve# cmdui.py - A widget to execute Mercurial command for TortoiseHg # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciScintilla from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _, localgettext from tortoisehg.hgqt import cmdcore, qtlib, qscilib local = localgettext() def startProgress(topic, status): topic, item, pos, total, unit = topic, '...', status, None, '' return (topic, pos, item, unit, total) def stopProgress(topic): topic, item, pos, total, unit = topic, '', None, None, '' return (topic, pos, item, unit, total) class ProgressMonitor(QWidget): 'Progress bar for use in workbench status bar' def __init__(self, topic, parent): super(ProgressMonitor, self).__init__(parent) hbox = QHBoxLayout() hbox.setContentsMargins(*(0,)*4) self.setLayout(hbox) self.idle = False self.pbar = QProgressBar() self.pbar.setTextVisible(False) self.pbar.setMinimum(0) hbox.addWidget(self.pbar) self.topic = QLabel(topic) hbox.addWidget(self.topic, 0) self.status = QLabel() hbox.addWidget(self.status, 1) self.pbar.setMaximum(100) self.pbar.reset() self.status.setText('') def clear(self): self.pbar.setMinimum(0) self.pbar.setMaximum(100) self.pbar.setValue(100) self.status.setText('') self.idle = True def setcounts(self, cur, max): # cur and max may exceed INT_MAX, which confuses QProgressBar assert max != 0 self.pbar.setMaximum(100) self.pbar.setValue(int(cur * 100 / max)) def unknown(self): self.pbar.setMinimum(0) self.pbar.setMaximum(0) class ThgStatusBar(QStatusBar): linkActivated = pyqtSignal(QString) def __init__(self, parent=None): QStatusBar.__init__(self, parent) self.topics = {} self.lbl = QLabel() self.lbl.linkActivated.connect(self.linkActivated) self.addWidget(self.lbl) self.setStyleSheet('QStatusBar::item { border: none }') @pyqtSlot(unicode) def showMessage(self, ustr, error=False): self.lbl.setText(ustr) if error: self.lbl.setStyleSheet('QLabel { color: red }') else: self.lbl.setStyleSheet('') def clear(self): keys = self.topics.keys() for key in keys: pm = self.topics[key] self.removeWidget(pm) del self.topics[key] @pyqtSlot(QString, object, QString, QString, object) def progress(self, topic, pos, item, unit, total, root=None): 'Progress signal received from repowidget' # topic is current operation # pos is the current numeric position (revision, bytes) # item is a non-numeric marker of current position (current file) # unit is a string label # total is the highest expected pos # # All topics should be marked closed by setting pos to None if root: key = (root, topic) else: key = topic if pos is None or (not pos and not total): if key in self.topics: pm = self.topics[key] self.removeWidget(pm) del self.topics[key] return if key not in self.topics: pm = ProgressMonitor(topic, self) pm.setMaximumHeight(self.lbl.sizeHint().height()) self.addWidget(pm) self.topics[key] = pm else: pm = self.topics[key] if total: fmt = '%s / %s ' % (unicode(pos), unicode(total)) if unit: fmt += unit pm.status.setText(fmt) pm.setcounts(pos, total) else: if item: item = item[-30:] pm.status.setText('%s %s' % (unicode(pos), item)) pm.unknown() def updateStatusMessage(stbar, session): """Update status bar to show the status of the given session""" if not session.isFinished(): stbar.showMessage(_('Running...')) elif session.isAborted(): stbar.showMessage(_('Terminated by user')) elif session.exitCode() == 0: stbar.showMessage(_('Finished')) else: stbar.showMessage(_('Failed!'), True) class Core(QObject): """Core functionality for running Mercurial command. Do not attempt to instantiate and use this directly. """ commandStarted = pyqtSignal() commandFinished = pyqtSignal(int) commandCanceling = pyqtSignal() output = pyqtSignal(QString, QString) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, logWindow, parent): super(Core, self).__init__(parent) self._agent = cmdcore.CmdAgent(self) self._session = cmdcore.nullCmdSession() self.rawoutlines = [] self.stbar = None if logWindow: self.outputLog = LogWidget() self.outputLog.installEventFilter(qscilib.KeyPressInterceptor(self)) self.output.connect(self.outputLog.appendLog) ### Public Methods ### def run(self, cmdline, *cmdlines, **opts): '''Execute or queue Mercurial command''' display = opts.get('display') worker = opts.get('useproc', False) and 'proc' or None ucmdlines = [map(hglib.tounicode, xs) for xs in (cmdline,) + cmdlines] udisplay = hglib.tounicode(display) sess = self._agent.runCommandSequence(ucmdlines, self, display=udisplay, worker=worker) sess.commandFinished.connect(self.onCommandFinished) sess.outputReceived.connect(self.onOutputReceived) sess.progressReceived.connect(self.onProgressReceived) self._session = sess # client widget assumes that the command starts immediately self.onCommandStarted() def cancel(self): '''Cancel running Mercurial command''' if self.running(): self._session.abort() self.commandCanceling.emit() def running(self): # pending session is "running" in cmdui world return not self._session.isFinished() def rawoutput(self): return ''.join(self.rawoutlines) def setStbar(self, stbar): self.stbar = stbar ### Private Method ### def clearOutput(self): if hasattr(self, 'outputLog'): self.outputLog.clearLog() ### Signal Handlers ### def onCommandStarted(self): self.rawoutlines = [] self.commandStarted.emit() if self.stbar: updateStatusMessage(self.stbar, self._session) @pyqtSlot(int) def onCommandFinished(self, ret): if self.stbar: updateStatusMessage(self.stbar, self._session) self.commandFinished.emit(ret) @pyqtSlot(QString, QString) def onOutputReceived(self, msg, label): if label != 'control': self.rawoutlines.append(hglib.fromunicode(msg, 'replace')) self.output.emit(msg, label) @pyqtSlot(QString, object, QString, QString, object) def onProgressReceived(self, topic, pos, item, unit, total): self.progress.emit(topic, pos, item, unit, total) if self.stbar: self.stbar.progress(topic, pos, item, unit, total) class LogWidget(qscilib.ScintillaCompat): """Output log viewer""" def __init__(self, parent=None): super(LogWidget, self).__init__(parent) self.setReadOnly(True) self.setUtf8(True) self.setMarginWidth(1, 0) self.setWrapMode(QsciScintilla.WrapCharacter) self._initfont() self._initmarkers() qscilib.unbindConflictedKeys(self) def _initfont(self): tf = qtlib.getfont('fontoutputlog') tf.changed.connect(self.forwardFont) self.setFont(tf.font()) @pyqtSlot(QFont) def forwardFont(self, font): self.setFont(font) def _initmarkers(self): self._markers = {} for l in ('ui.error', 'control'): self._markers[l] = m = self.markerDefine(QsciScintilla.Background) c = QColor(qtlib.getbgcoloreffect(l)) if c.isValid(): self.setMarkerBackgroundColor(c, m) # NOTE: self.setMarkerForegroundColor() doesn't take effect, # because it's a *Background* marker. @pyqtSlot(unicode, str) def appendLog(self, msg, label): """Append log text to the last line; scrolls down to there""" self.append(msg) self._setmarker(xrange(self.lines() - unicode(msg).count('\n') - 1, self.lines() - 1), label) self.setCursorPosition(self.lines() - 1, 0) def _setmarker(self, lines, label): for m in self._markersforlabel(label): for i in lines: self.markerAdd(i, m) def _markersforlabel(self, label): return iter(self._markers[l] for l in str(label).split() if l in self._markers) @pyqtSlot() def clearLog(self): """This slot can be overridden by subclass to do more actions""" self.clear() def contextMenuEvent(self, event): menu = self.createStandardContextMenu() menu.addSeparator() menu.addAction(_('Clea&r Log'), self.clearLog) menu.exec_(event.globalPos()) menu.setParent(None) class Widget(QWidget): """An embeddable widget for running Mercurial command""" commandStarted = pyqtSignal() commandFinished = pyqtSignal(int) commandCanceling = pyqtSignal() output = pyqtSignal(QString, QString) progress = pyqtSignal(QString, object, QString, QString, object) makeLogVisible = pyqtSignal(bool) def __init__(self, logWindow, statusBar, parent): super(Widget, self).__init__(parent) self.core = Core(logWindow, self) self.core.commandStarted.connect(self.commandStarted) self.core.commandFinished.connect(self.onCommandFinished) self.core.commandCanceling.connect(self.commandCanceling) self.core.output.connect(self.output) self.core.progress.connect(self.progress) if not logWindow: return vbox = QVBoxLayout() vbox.setSpacing(4) vbox.setContentsMargins(*(1,)*4) self.setLayout(vbox) # command output area self.core.outputLog.setHidden(True) self.layout().addWidget(self.core.outputLog, 1) if statusBar: ## status and progress labels self.stbar = ThgStatusBar() self.stbar.setSizeGripEnabled(False) self.core.setStbar(self.stbar) self.layout().addWidget(self.stbar) ### Public Methods ### def run(self, cmdline, *args, **opts): self.core.run(cmdline, *args, **opts) def cancel(self): self.core.cancel() def setShowOutput(self, visible): if hasattr(self.core, 'outputLog'): self.core.outputLog.setShown(visible) def outputShown(self): if hasattr(self.core, 'outputLog'): return self.core.outputLog.isVisible() else: return False ### Signal Handler ### @pyqtSlot(int) def onCommandFinished(self, ret): if ret == -1: self.makeLogVisible.emit(True) self.setShowOutput(True) self.commandFinished.emit(ret) class CmdSessionDialog(QDialog): """Dialog to monitor running Mercurial commands""" def __init__(self, parent=None): super(CmdSessionDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) vbox = QVBoxLayout() vbox.setSpacing(4) vbox.setContentsMargins(5, 5, 5, 5) # command output area self._outputLog = LogWidget(self) self._outputLog.installEventFilter(qscilib.KeyPressInterceptor(self)) vbox.addWidget(self._outputLog, 1) ## status and progress labels self._stbar = ThgStatusBar() self._stbar.setSizeGripEnabled(False) vbox.addWidget(self._stbar) # bottom buttons buttons = QDialogButtonBox() self._cancelBtn = buttons.addButton(QDialogButtonBox.Cancel) self._cancelBtn.clicked.connect(self.abortCommand) self._closeBtn = buttons.addButton(QDialogButtonBox.Close) self._closeBtn.clicked.connect(self.reject) self._detailBtn = buttons.addButton(_('Detail'), QDialogButtonBox.ResetRole) self._detailBtn.setAutoDefault(False) self._detailBtn.setCheckable(True) self._detailBtn.setChecked(True) self._detailBtn.toggled.connect(self.setLogVisible) vbox.addWidget(buttons) self.setLayout(vbox) self.setWindowTitle(_('TortoiseHg Command Dialog')) self.resize(540, 420) self._session = cmdcore.nullCmdSession() self._updateUi() def setSession(self, sess): """Start watching the given command session""" assert self._session.isFinished() self._session = sess sess.commandFinished.connect(self._updateUi) sess.outputReceived.connect(self._outputLog.appendLog) sess.progressReceived.connect(self._stbar.progress) self._updateUi() @pyqtSlot() def abortCommand(self): self._session.abort() self._cancelBtn.setDisabled(True) def setLogVisible(self, visible): """show/hide command output""" self._outputLog.setVisible(visible) self._detailBtn.setChecked(visible) # workaround to adjust only window height self.setMinimumWidth(self.width()) self.adjustSize() self.setMinimumWidth(0) def reject(self): if not self._session.isFinished(): ret = QMessageBox.question(self, _('Confirm Exit'), _('Mercurial command is still running.\n' 'Are you sure you want to terminate?'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if ret == QMessageBox.Yes: self.abortCommand() # don't close dialog return # close dialog if self._session.exitCode() == 0: self.accept() # means command successfully finished else: super(CmdSessionDialog, self).reject() @pyqtSlot() def _updateUi(self): updateStatusMessage(self._stbar, self._session) self._cancelBtn.setVisible(not self._session.isFinished()) self._closeBtn.setVisible(self._session.isFinished()) if self._session.isFinished(): self._closeBtn.setFocus() def errorMessageBox(session, parent=None, title=None): """Open a message box to report the error of the given session""" if not title: title = _('Command Error') reason = session.errorString() text = session.warningString() if text: text += '\n\n' text += _('[Code: %d]') % session.exitCode() return qtlib.WarningMsgBox(title, reason, text, parent=parent) tortoisehg-2.10/tortoisehg/hgqt/serve_ui.py0000644000076400007640000001042512212224146020214 0ustar stevesteve# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/steve/repos/thg/tortoisehg/hgqt/serve.ui' # # Created: Thu Sep 5 19:57:10 2013 # by: PyQt4 UI code generator 4.6.2 # # WARNING! All changes made in this file will be lost! from tortoisehg.hgqt.i18n import _ from PyQt4 import QtCore, QtGui class Ui_ServeDialog(object): def setupUi(self, ServeDialog): ServeDialog.setObjectName("ServeDialog") ServeDialog.resize(500, 400) self.dialog_layout = QtGui.QVBoxLayout(ServeDialog) self.dialog_layout.setObjectName("dialog_layout") self.top_layout = QtGui.QHBoxLayout() self.top_layout.setObjectName("top_layout") self.opts_layout = QtGui.QFormLayout() self.opts_layout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow) self.opts_layout.setObjectName("opts_layout") self.port_label = QtGui.QLabel(ServeDialog) self.port_label.setObjectName("port_label") self.opts_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.port_label) self.port_edit = QtGui.QSpinBox(ServeDialog) self.port_edit.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.port_edit.setMinimum(1) self.port_edit.setMaximum(65535) self.port_edit.setProperty("value", 8000) self.port_edit.setObjectName("port_edit") self.opts_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.port_edit) self.status_label = QtGui.QLabel(ServeDialog) self.status_label.setObjectName("status_label") self.opts_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.status_label) self.status_edit = QtGui.QLabel(ServeDialog) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.status_edit.sizePolicy().hasHeightForWidth()) self.status_edit.setSizePolicy(sizePolicy) self.status_edit.setTextFormat(QtCore.Qt.RichText) self.status_edit.setOpenExternalLinks(True) self.status_edit.setObjectName("status_edit") self.opts_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.status_edit) self.top_layout.addLayout(self.opts_layout) self.actions_layout = QtGui.QVBoxLayout() self.actions_layout.setObjectName("actions_layout") self.start_button = QtGui.QPushButton(ServeDialog) self.start_button.setDefault(True) self.start_button.setObjectName("start_button") self.actions_layout.addWidget(self.start_button) self.stop_button = QtGui.QPushButton(ServeDialog) self.stop_button.setAutoDefault(False) self.stop_button.setObjectName("stop_button") self.actions_layout.addWidget(self.stop_button) spacerItem = QtGui.QSpacerItem(0, 5, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.actions_layout.addItem(spacerItem) self.settings_button = QtGui.QPushButton(ServeDialog) self.settings_button.setAutoDefault(False) self.settings_button.setObjectName("settings_button") self.actions_layout.addWidget(self.settings_button) self.top_layout.addLayout(self.actions_layout) self.dialog_layout.addLayout(self.top_layout) self.details_tabs = QtGui.QTabWidget(ServeDialog) self.details_tabs.setObjectName("details_tabs") self.dialog_layout.addWidget(self.details_tabs) self.port_label.setBuddy(self.port_edit) self.retranslateUi(ServeDialog) self.details_tabs.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(ServeDialog) ServeDialog.setTabOrder(self.port_edit, self.start_button) ServeDialog.setTabOrder(self.start_button, self.stop_button) ServeDialog.setTabOrder(self.stop_button, self.settings_button) ServeDialog.setTabOrder(self.settings_button, self.details_tabs) def retranslateUi(self, ServeDialog): ServeDialog.setWindowTitle(_('Web Server')) self.port_label.setText(_('Port:')) self.status_label.setText(_('Status:')) self.start_button.setText(_('Start')) self.stop_button.setText(_('Stop')) self.settings_button.setText(_('Settings')) tortoisehg-2.10/tortoisehg/hgqt/revset.py0000644000076400007640000003600312170335562017713 0ustar stevesteve# revset.py - revision set query dialog # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import revset, hg, error from tortoisehg.hgqt import qtlib, cmdui from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from PyQt4.Qsci import QsciScintilla, QsciAPIs, QsciLexerPython from PyQt4.QtCore import * from PyQt4.QtGui import * # TODO: # Connect to repoview revisionClicked events # Shift-Click rev range -> revision range X:Y # Ctrl-Click two revs -> DAG range X::Y # QFontMetrics.elidedText for help label _common = ( ('user(string)', _('Changesets where username contains string.')), ('keyword(string)', _('Search commit message, user name, and names of changed ' 'files for string.')), ('grep(regex)', _('Like "keyword(string)" but accepts a regex.')), ('outgoing([path])', _('Changesets not found in the specified destination repository, ' 'or the default push location.')), ('bookmark([name])', _('The named bookmark or all bookmarks.')), ('tag([name])', _('The named tag or all tags.')), ('tagged()', _('Changeset is tagged.')), ('head()', _('Changeset is a named branch head.')), ('merge()', _('Changeset is a merge changeset.')), ('closed()', _('Changeset is closed.')), ('date(interval)', _('Changesets within the interval, see help dates')), ('ancestor(single, single)', _('Greatest common ancestor of the two changesets.')), ('matching(revset [, ''field(s) to match''])', _('Find revisions that "match" one or more fields of the given set of ' 'revisions.')), ) _filepatterns = ( ('file(pattern)', _('Changesets affecting files matched by pattern. ' 'See ' 'help patterns')), ('modifies(pattern)', _('Changesets which modify files matched by pattern.')), ('adds(pattern)', _('Changesets which add files matched by pattern.')), ('removes(pattern)', _('Changesets which remove files matched by pattern.')), ('contains(pattern)', _('Changesets containing files matched by pattern.')), ) _ancestry = ( ('branch(set)', _('All changesets belonging to the branches of changesets in set.')), ('heads(set)', _('Members of a set with no children in set.')), ('descendants(set)', _('Changesets which are descendants of changesets in set.')), ('ancestors(set)', _('Changesets that are ancestors of a changeset in set.')), ('children(set)', _('Child changesets of changesets in set.')), ('parents(set)', _('The set of all parents for all changesets in set.')), ('p1(set)', _('First parent for all changesets in set, or the working directory.')), ('p2(set)', _('Second parent for all changesets in set, or the working directory.')), ('roots(set)', _('Changesets with no parent changeset in set.')), ('present(set)', _('An empty set, if any revision in set isn\'t found; otherwise, ' 'all revisions in set.')), ) _logical = ( ('min(set)', _('Changeset with lowest revision number in set.')), ('max(set)', _('Changeset with highest revision number in set.')), ('limit(set, n)', _('First n members of a set.')), ('sort(set[, [-]key...])', _('Sort set by keys. The default sort order is ascending, specify a ' 'key as "-key" to sort in descending order.')), ('follow()', _('An alias for "::." (ancestors of the working copy\'s first parent).')), ('all()', _('All changesets, the same as 0:tip.')), ) class RevisionSetQuery(QDialog): # Emit query string and resulting revision set queryIssued = pyqtSignal(QString, object) showMessage = pyqtSignal(QString) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repo, parent=None): QDialog.__init__(self, parent) self.repo = repo # Since the revset dialot belongs to a repository, we display # the repository name in the dialog title self.setWindowTitle(_('Revision Set Query') + ' - ' + repo.displayname) self.setWindowFlags(Qt.Window) layout = QVBoxLayout() layout.setMargin(0) layout.setContentsMargins(*(4,)*4) self.setLayout(layout) logical = _logical ancestry = _ancestry if 'hgsubversion' in repo.extensions(): logical = list(logical) + [('fromsvn()', _('all revisions converted from subversion')),] ancestry = list(ancestry) + [('svnrev(rev)', _('changeset which represents converted svn revision')),] self.stbar = cmdui.ThgStatusBar(self) self.stbar.setSizeGripEnabled(False) self.stbar.lbl.setOpenExternalLinks(True) self.showMessage.connect(self.stbar.showMessage) self.progress.connect(self.stbar.progress) hbox = QHBoxLayout() hbox.setContentsMargins(*(0,)*4) cgb = QGroupBox(_('Common sets')) cgb.setLayout(QVBoxLayout()) cgb.layout().setContentsMargins(*(2,)*4) def setCommonHelp(row): self.stbar.showMessage(self.clw._help[row]) self.clw = QListWidget(self) self.clw.addItems([x for x, y in _common]) self.clw._help = [y for x, y in _common] self.clw.currentRowChanged.connect(setCommonHelp) cgb.layout().addWidget(self.clw) hbox.addWidget(cgb) fgb = QGroupBox(_('File pattern sets')) fgb.setLayout(QVBoxLayout()) fgb.layout().setContentsMargins(*(2,)*4) def setFileHelp(row): self.stbar.showMessage(self.flw._help[row]) self.flw = QListWidget(self) self.flw.addItems([x for x, y in _filepatterns]) self.flw._help = [y for x, y in _filepatterns] self.flw.currentRowChanged.connect(setFileHelp) fgb.layout().addWidget(self.flw) hbox.addWidget(fgb) agb = QGroupBox(_('Set Ancestry')) agb.setLayout(QVBoxLayout()) agb.layout().setContentsMargins(*(2,)*4) def setAncHelp(row): self.stbar.showMessage(self.alw._help[row]) self.alw = QListWidget(self) self.alw.addItems([x for x, y in ancestry]) self.alw._help = [y for x, y in ancestry] self.alw.currentRowChanged.connect(setAncHelp) agb.layout().addWidget(self.alw) hbox.addWidget(agb) lgb = QGroupBox(_('Set Logic')) lgb.setLayout(QVBoxLayout()) lgb.layout().setContentsMargins(*(2,)*4) def setManipHelp(row): self.stbar.showMessage(self.llw._help[row]) self.llw = QListWidget(self) self.llw.addItems([x for x, y in logical]) self.llw._help = [y for x, y in logical] self.llw.currentRowChanged.connect(setManipHelp) lgb.layout().addWidget(self.llw) hbox.addWidget(lgb) # Clicking on one listwidget should clear selection of the others listwidgets = (self.clw, self.flw, self.alw, self.llw) for w in listwidgets: w.currentItemChanged.connect(self.currentItemChanged) #w.itemActivated.connect(self.returnPressed) for w2 in listwidgets: if w is not w2: w.itemClicked.connect(w2.clearSelection) layout.addLayout(hbox, 1) self.entry = RevsetEntry(self) self.entry.addCompletions(logical, ancestry, _filepatterns, _common) self.entry.returnPressed.connect(self.returnPressed) layout.addWidget(self.entry, 0) txt = _('' 'help revsets') helpLabel = QLabel(txt) helpLabel.setOpenExternalLinks(True) self.stbar.addPermanentWidget(helpLabel) layout.addWidget(self.stbar, 0) QShortcut(QKeySequence('Return'), self, self.returnPressed) QShortcut(QKeySequence('Escape'), self, self.reject) self.refreshing = None def runQuery(self): if self.refreshing: return self.entry.setEnabled(False) self.showMessage.emit(_('Searching...')) self.progress.emit(*cmdui.startProgress(_('Running'), _('query'))) self.refreshing = RevsetThread(self.repo, self.entry.text(), self) self.refreshing.showMessage.connect(self.showMessage) self.refreshing.queryIssued.connect(self.queryIssued) self.refreshing.finished.connect(self.queryFinished) self.refreshing.setCursorPosition.connect(self.entry.setCursorPosition) self.refreshing.start() def queryFinished(self): self.refreshing.wait() self.refreshing.setParent(None) # assist garbage-collection self.refreshing = None self.entry.setEnabled(True) self.progress.emit(*cmdui.stopProgress(_('Running'))) def returnPressed(self): if self.entry.hasSelectedText(): lineFrom, indexFrom, lineTo, indexTo = self.entry.getSelection() start = self.entry.positionFromLineIndex(lineFrom, indexFrom) end = self.entry.positionFromLineIndex(lineTo, indexTo) sel = self.entry.selectedText() if sel.count('(') and sel.contains(')'): bopen = sel.indexOf('(') bclose = sel.lastIndexOf(')') if bopen < bclose: self.entry.setSelection(lineFrom, start+bopen+1, lineFrom, start+bclose) self.entry.setFocus() return self.entry.setSelection(lineTo, indexTo, lineTo, indexTo) else: self.runQuery() self.entry.setFocus() def currentItemChanged(self, current, previous): if current is None: return self.entry.beginUndoAction() text = self.entry.text() itext, ilen = current.text(), len(current.text()) if self.entry.hasSelectedText(): # replace selection lineFrom, indexFrom, lineTo, indexTo = self.entry.getSelection() start = self.entry.positionFromLineIndex(lineFrom, indexFrom) end = self.entry.positionFromLineIndex(lineTo, indexTo) newtext = text[:start] + itext + text[end:] self.entry.setText(newtext) self.entry.setSelection(lineFrom, indexFrom, lineFrom, indexFrom+ilen) else: line, index = self.entry.getCursorPosition() pos = self.entry.positionFromLineIndex(line, index) if len(text) <= pos: # cursor at end of text, append if text and text[-1] != u' ': text = text + u' ' newtext = text + itext self.entry.setText(newtext) self.entry.setSelection(line, len(text), line, len(newtext)) elif text[pos] == u' ': # cursor is at a space, insert item newtext = text[:pos] + itext + text[pos:] self.entry.setText(newtext) self.entry.setSelection(line, pos, line, pos+ilen) else: # cursor is on text, wrap current word start, end = pos, pos while start and text[start-1] != u' ': start = start-1 while end < len(text) and text[end] != u' ': end = end+1 bopen = itext.indexOf('(') newtext = text[:start] + itext[:bopen+1] + text[start:end] + \ ')' + text[end:] self.entry.setText(newtext) self.entry.setSelection(line, start, line, end+bopen+2) self.entry.endUndoAction() def accept(self): self.hide() def reject(self): self.accept() class RevsetEntry(QsciScintilla): returnPressed = pyqtSignal() def __init__(self, parent=None): super(RevsetEntry, self).__init__(parent) self.setMarginWidth(1, 0) self.setReadOnly(False) self.setUtf8(True) self.setCaretWidth(10) self.setCaretLineBackgroundColor(QColor("#e6fff0")) self.setCaretLineVisible(True) self.setAutoIndent(True) self.setMatchedBraceBackgroundColor(Qt.yellow) self.setIndentationsUseTabs(False) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) self.setWrapMode(QsciScintilla.WrapWord) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sp.setHorizontalStretch(1) sp.setVerticalStretch(0) self.setSizePolicy(sp) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.setAutoCompletionFillupsEnabled(True) self.setLexer(QsciLexerPython(self)) self.lexer().setFont(qtlib.getfont('fontcomment').font()) self.apis = QsciAPIs(self.lexer()) def addCompletions(self, *lists): for list in lists: for x, y in list: self.apis.add(x) self.apis.prepare() def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: event.ignore() return if event.key() in (Qt.Key_Enter, Qt.Key_Return): if not self.isListActive(): event.ignore() self.returnPressed.emit() return super(RevsetEntry, self).keyPressEvent(event) def sizeHint(self): return QSize(10, self.fontMetrics().height()) class RevsetThread(QThread): queryIssued = pyqtSignal(QString, object) showMessage = pyqtSignal(QString) setCursorPosition = pyqtSignal(int, int) def __init__(self, repo, query, parent): super(RevsetThread, self).__init__(parent) self.repo = hg.repository(repo.ui, repo.root) self.text = hglib.fromunicode(query) self.query = query def run(self): cwd = os.getcwd() try: os.chdir(self.repo.root) func = revset.match(self.repo.ui, self.text) l = list(func(self.repo, list(self.repo))) if len(l): self.showMessage.emit(_('%d matches found') % len(l)) else: self.showMessage.emit(_('No matches found')) self.queryIssued.emit(self.query, l) except error.ParseError, e: if len(e.args) == 2: msg, pos = e.args self.setCursorPosition.emit(0, pos) else: msg = e.args[0] self.showMessage.emit(_('Parse Error: ') + hglib.tounicode(msg)) except TypeError: raise except Exception, e: self.showMessage.emit(_('Invalid query: ')+hglib.tounicode(str(e))) os.chdir(cwd) tortoisehg-2.10/tortoisehg/hgqt/logcolumns.py0000644000076400007640000000655712110205646020571 0ustar stevesteve# logcolumns.py - select and reorder columns in log model # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from tortoisehg.hgqt import qtlib from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import repomodel from PyQt4.QtCore import * from PyQt4.QtGui import * class ColumnSelectDialog(QDialog): def __init__(self, cfgname, name, model, parent=None): QDialog.__init__(self, parent) if model: all = model._allcolumns colnames = model._allcolnames self.curcolumns = model._columns else: all = repomodel.HgRepoListModel._allcolumns colnames = repomodel.HgRepoListModel._allcolnames self.curcolumns = None self.setWindowTitle(name) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self.setMinimumSize(250, 265) self.cfgname = cfgname if not self.curcolumns: s = QSettings() cols = s.value(self.cfgname + '/columns').toStringList() if cols: self.curcolumns = [hglib.fromunicode(c) for c in cols if c in all] else: self.curcolumns = all self.disabled = [c for c in all if c not in self.curcolumns] layout = QVBoxLayout() layout.setContentsMargins(5, 5, 5, 5) self.setLayout(layout) list = QListWidget() # enabled cols are listed in sorted order for c in self.curcolumns: item = QListWidgetItem(colnames[c]) item.columnid = c item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) list.addItem(item) # disabled cols are listed last for c in self.disabled: item = QListWidgetItem(colnames[c]) item.columnid = c item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) list.addItem(item) list.setDragDropMode(QListView.InternalMove) layout.addWidget(list) self.list = list layout.addWidget(QLabel(_('Drag to change order'))) # dialog buttons BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) self.apply_button = bb.button(BB.Apply) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) layout.addWidget(bb) def accept(self): s = QSettings() cols = [] for i in xrange(self.list.count()): item = self.list.item(i) if item.checkState() == Qt.Checked: cols.append(item.columnid) s.setValue(self.cfgname + '/columns', cols) QDialog.accept(self) def reject(self): QDialog.reject(self) def run(ui, *pats, **opts): return ColumnSelectDialog('workbench', _('Workbench'), None) tortoisehg-2.10/tortoisehg/hgqt/serve.ui0000644000076400007640000001004612110205646017504 0ustar stevesteve ServeDialog 0 0 500 400 Web Server QFormLayout::ExpandingFieldsGrow Port: port_edit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 65535 8000 Status: 0 0 Qt::RichText true Start true Stop false Qt::Vertical QSizePolicy::Expanding 0 5 Settings false -1 port_edit start_button stop_button settings_button details_tabs tortoisehg-2.10/tortoisehg/hgqt/guess.py0000644000076400007640000003774512231647662017554 0ustar stevesteve# guess.py - TortoiseHg's dialogs for detecting copies and renames # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import hg, ui, mdiff, similar, patch from tortoisehg.util import hglib, shlib, thread2 from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, htmlui, cmdui from PyQt4.QtCore import * from PyQt4.QtGui import * # Techincal debt # Try to cut down on the jitter when findRenames is pressed. May # require a splitter. class DetectRenameDialog(QDialog): 'Detect renames after they occur' matchAccepted = pyqtSignal() def __init__(self, repoagent, parent, *pats): QDialog.__init__(self, parent) self._repoagent = repoagent repo = repoagent.rawRepo() self.pats = pats self.thread = None self.setWindowTitle(_('Detect Copies/Renames in %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('detect_rename')) self.setWindowFlags(Qt.Window) layout = QVBoxLayout() layout.setContentsMargins(*(2,)*4) self.setLayout(layout) # vsplit for top & diff vsplit = QSplitter(Qt.Horizontal) utframe = QFrame(vsplit) matchframe = QFrame(vsplit) utvbox = QVBoxLayout() utvbox.setContentsMargins(*(2,)*4) utframe.setLayout(utvbox) matchvbox = QVBoxLayout() matchvbox.setContentsMargins(*(2,)*4) matchframe.setLayout(matchvbox) hsplit = QSplitter(Qt.Vertical) layout.addWidget(hsplit) hsplit.addWidget(vsplit) utheader = QHBoxLayout() utvbox.addLayout(utheader) utlbl = QLabel(_('Unrevisioned Files')) utheader.addWidget(utlbl) self.refreshBtn = tb = QToolButton() tb.setToolTip(_('Refresh file list')) tb.setIcon(qtlib.geticon('view-refresh')) tb.clicked.connect(self.refresh) utheader.addWidget(tb) self.unrevlist = QListWidget() self.unrevlist.setSelectionMode(QAbstractItemView.ExtendedSelection) self.unrevlist.doubleClicked.connect(self.onUnrevDoubleClicked) utvbox.addWidget(self.unrevlist) simhbox = QHBoxLayout() utvbox.addLayout(simhbox) lbl = QLabel() slider = QSlider(Qt.Horizontal) slider.setRange(0, 100) slider.setTickInterval(10) slider.setPageStep(10) slider.setTickPosition(QSlider.TicksBelow) slider.changefunc = lambda v: lbl.setText( _('Min Similarity: %d%%') % v) slider.valueChanged.connect(slider.changefunc) self.simslider = slider lbl.setBuddy(slider) simhbox.addWidget(lbl) simhbox.addWidget(slider, 1) buthbox = QHBoxLayout() utvbox.addLayout(buthbox) copycheck = QCheckBox(_('Only consider deleted files')) copycheck.setToolTip(_('Uncheck to consider all revisioned files ' 'for copy sources')) copycheck.setChecked(True) findrenames = QPushButton(_('Find Renames')) findrenames.setToolTip(_('Find copy and/or rename sources')) findrenames.setEnabled(False) findrenames.clicked.connect(self.findRenames) buthbox.addWidget(copycheck) buthbox.addStretch(1) buthbox.addWidget(findrenames) self.findbtn, self.copycheck = findrenames, copycheck matchlbl = QLabel(_('Candidate Matches')) matchvbox.addWidget(matchlbl) matchtv = QTreeView() matchtv.setSelectionMode(QTreeView.ExtendedSelection) matchtv.setItemsExpandable(False) matchtv.setRootIsDecorated(False) matchtv.setModel(MatchModel()) matchtv.setSortingEnabled(True) matchtv.selectionModel().selectionChanged.connect(self.showDiff) buthbox = QHBoxLayout() matchbtn = QPushButton(_('Accept All Matches')) matchbtn.clicked.connect(self.acceptMatch) matchbtn.setEnabled(False) buthbox.addStretch(1) buthbox.addWidget(matchbtn) matchvbox.addWidget(matchtv) matchvbox.addLayout(buthbox) self.matchtv, self.matchbtn = matchtv, matchbtn def matchselect(s, d): count = len(matchtv.selectedIndexes()) if count: self.matchbtn.setText(_('Accept Selected Matches')) else: self.matchbtn.setText(_('Accept All Matches')) selmodel = matchtv.selectionModel() selmodel.selectionChanged.connect(matchselect) sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sp.setHorizontalStretch(1) matchframe.setSizePolicy(sp) diffframe = QFrame(hsplit) diffvbox = QVBoxLayout() diffvbox.setContentsMargins(*(2,)*4) diffframe.setLayout(diffvbox) difflabel = QLabel(_('Differences from Source to Dest')) diffvbox.addWidget(difflabel) difftb = QTextBrowser() difftb.document().setDefaultStyleSheet(qtlib.thgstylesheet) diffvbox.addWidget(difftb) self.difftb = difftb self.stbar = cmdui.ThgStatusBar() layout.addWidget(self.stbar) s = QSettings() self.restoreGeometry(s.value('guess/geom').toByteArray()) hsplit.restoreState(s.value('guess/hsplit-state').toByteArray()) vsplit.restoreState(s.value('guess/vsplit-state').toByteArray()) slider.setValue(s.value('guess/simslider').toInt()[0] or 50) self.vsplit, self.hsplit = vsplit, hsplit QTimer.singleShot(0, self.refresh) @property def repo(self): return self._repoagent.rawRepo() def refresh(self): self.repo.thginvalidate() self.repo.lfstatus = True wctx = self.repo[None] wctx.status(unknown=True) self.repo.lfstatus = False self.unrevlist.clear() dests = [] for u in wctx.unknown(): dests.append(u) for a in wctx.added(): if not wctx[a].renamed(): dests.append(a) for x in dests: item = QListWidgetItem(hglib.tounicode(x)) item.orig = x self.unrevlist.addItem(item) self.unrevlist.setItemSelected(item, x in self.pats) if dests: self.findbtn.setEnabled(True) else: self.findbtn.setEnabled(False) self.difftb.clear() self.pats = [] self.matchbtn.setEnabled(len(self.matchtv.model().rows)) def findRenames(self): 'User pressed "find renames" button' if self.thread and self.thread.isRunning(): QMessageBox.information(self, _('Search already in progress'), _('Cannot start a new search')) return ulist = [it.orig for it in self.unrevlist.selectedItems()] if not ulist: # When no files are selected, look for all files ulist = [self.unrevlist.item(n).orig for n in range(self.unrevlist.count())] if not ulist: QMessageBox.information(self, _('No files to find'), _('There are no files that may have been renamed')) return pct = self.simslider.value() / 100.0 copies = not self.copycheck.isChecked() self.findbtn.setEnabled(False) self.matchtv.model().clear() self.thread = RenameSearchThread(self.repo, ulist, pct, copies) self.thread.match.connect(self.rowReceived) self.thread.progress.connect(self.stbar.progress) self.thread.showMessage.connect(self.stbar.showMessage) self.thread.finished.connect(self.searchfinished) self.thread.start() def searchfinished(self): self.stbar.clear() for col in xrange(3): self.matchtv.resizeColumnToContents(col) self.findbtn.setEnabled(self.unrevlist.count()) self.matchbtn.setEnabled(len(self.matchtv.model().rows)) def rowReceived(self, args): self.matchtv.model().appendRow(*args) def acceptMatch(self): 'User pressed "accept match" button' remdests = {} wctx = self.repo[None] m = self.matchtv.model() # If no rows are selected, ask the user if he'd like to accept all renames if self.matchtv.selectionModel().hasSelection(): itemList = [self.matchtv.model().getRow(index) \ for index in self.matchtv.selectionModel().selectedRows()] else: itemList = m.rows for item in itemList: src, dest, percent = item if dest in remdests: udest = hglib.tounicode(dest) QMessageBox.warning(self, _('Multiple sources chosen'), _('You have multiple renames selected for ' 'destination file:\n%s. Aborting!') % udest) return remdests[dest] = src for dest, src in remdests.iteritems(): if not os.path.exists(self.repo.wjoin(src)): wctx.forget([src]) # !->R wctx.copy(src, dest) self.matchtv.model().remove(dest) self.matchAccepted.emit() self.refresh() def showDiff(self, index): 'User selected a row in the candidate tree' indexes = index.indexes() if not indexes: return index = indexes[0] ctx = self.repo['.'] hu = htmlui.htmlui() row = self.matchtv.model().getRow(index) src, dest, percent = self.matchtv.model().getRow(index) aa = self.repo.wread(dest) rr = ctx.filectx(src).data() date = hglib.displaytime(ctx.date()) difftext = mdiff.unidiff(rr, date, aa, date, src, dest) if not difftext: t = _('%s and %s have identical contents\n\n') % \ (hglib.tounicode(src), hglib.tounicode(dest)) hu.write(t, label='ui.error') else: for t, l in patch.difflabel(difftext.splitlines, True): hu.write(t, label=l) self.difftb.setHtml(hu.getdata()[0]) def onUnrevDoubleClicked(self, index): file = hglib.fromunicode(self.unrevlist.model().data(index).toString()) qtlib.editfiles(self.repo, [file]) def accept(self): s = QSettings() s.setValue('guess/geom', self.saveGeometry()) s.setValue('guess/vsplit-state', self.vsplit.saveState()) s.setValue('guess/hsplit-state', self.hsplit.saveState()) s.setValue('guess/simslider', self.simslider.value()) QDialog.accept(self) def reject(self): if self.thread and self.thread.isRunning(): self.thread.cancel() if self.thread.wait(2000): self.thread = None else: s = QSettings() s.setValue('guess/geom', self.saveGeometry()) s.setValue('guess/vsplit-state', self.vsplit.saveState()) s.setValue('guess/hsplit-state', self.hsplit.saveState()) s.setValue('guess/simslider', self.simslider.value()) QDialog.reject(self) def _aspercent(s): # i18n: percent format return _('%d%%') % (s * 100) class MatchModel(QAbstractTableModel): def __init__(self, parent=None): QAbstractTableModel.__init__(self, parent) self.rows = [] self.headers = (_('Source'), _('Dest'), _('% Match')) self.displayformats = (hglib.tounicode, hglib.tounicode, _aspercent) def rowCount(self, parent): return len(self.rows) def columnCount(self, parent): return len(self.headers) def data(self, index, role): if not index.isValid(): return QVariant() if role == Qt.DisplayRole: s = self.rows[index.row()][index.column()] f = self.displayformats[index.column()] return QVariant(f(s)) ''' elif role == Qt.TextColorRole: src, dst, pct = self.rows[index.row()] if pct == 1.0: return QColor('green') else: return QColor('black') elif role == Qt.ToolTipRole: # explain what row means? ''' return QVariant() def headerData(self, col, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() else: return QVariant(self.headers[col]) def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled # Custom methods def getRow(self, index): assert index.isValid() return self.rows[index.row()] def appendRow(self, *args): self.beginInsertRows(QModelIndex(), len(self.rows), len(self.rows)) self.rows.append(args) self.endInsertRows() self.layoutChanged.emit() def clear(self): self.beginRemoveRows(QModelIndex(), 0, len(self.rows)-1) self.rows = [] self.endRemoveRows() self.layoutChanged.emit() def remove(self, dest): i = 0 while i < len(self.rows): if self.rows[i][1] == dest: self.beginRemoveRows(QModelIndex(), i, i) self.rows.pop(i) self.endRemoveRows() else: i += 1 self.layoutChanged.emit() def sort(self, col, order): self.layoutAboutToBeChanged.emit() self.rows.sort(key=lambda x: x[col], reverse=(order == Qt.DescendingOrder)) self.layoutChanged.emit() self.reset() def isEmpty(self): return not bool(self.rows) class RenameSearchThread(QThread): '''Background thread for searching repository history''' match = pyqtSignal(object) progress = pyqtSignal(QString, object, QString, QString, object) showMessage = pyqtSignal(unicode) def __init__(self, repo, ufiles, minpct, copies): super(RenameSearchThread, self).__init__() self.repo = hg.repository(ui.ui(), repo.root) self.ufiles = ufiles self.minpct = minpct self.copies = copies self.threadid = None def run(self): def emit(topic, pos, item='', unit='', total=None): topic = hglib.tounicode(topic or '') item = hglib.tounicode(item or '') unit = hglib.tounicode(unit or '') self.progress.emit(topic, pos, item, unit, total) self.repo.ui.progress = emit self.threadid = int(self.currentThreadId()) try: try: self.search(self.repo) except KeyboardInterrupt: pass except Exception, e: self.showMessage.emit(hglib.tounicode(str(e))) finally: self.threadid = None def cancel(self): tid = self.threadid if tid is None: return try: thread2._async_raise(tid, KeyboardInterrupt) except ValueError: pass def search(self, repo): wctx = repo[None] pctx = repo['.'] if self.copies: wctx.status(clean=True) srcs = wctx.removed() + wctx.deleted() srcs += wctx.modified() + wctx.clean() else: srcs = wctx.removed() + wctx.deleted() added = [wctx[a] for a in self.ufiles] removed = [pctx[a] for a in srcs if a in pctx] # do not consider files of zero length added = sorted([fctx for fctx in added if fctx.size() > 0]) removed = sorted([fctx for fctx in removed if fctx.size() > 0]) exacts = [] gen = similar._findexactmatches(repo, added, removed) for o, n in gen: old, new = o.path(), n.path() exacts.append(old) self.match.emit([old, new, 1.0]) if self.minpct == 1.0: return removed = [r for r in removed if r.path() not in exacts] gen = similar._findsimilarmatches(repo, added, removed, self.minpct) for o, n, s in gen: old, new, sim = o.path(), n.path(), s self.match.emit([old, new, sim]) tortoisehg-2.10/tortoisehg/hgqt/mq.py0000644000076400007640000007377112235634453017040 0ustar stevesteve# mq.py - TortoiseHg MQ widget # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error, util from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, qtlib, cmdui, thgrepo from tortoisehg.hgqt import commit, qdelete, qfold, qrename, mqutil from tortoisehg.hgqt.qtlib import geticon class QueueManagementActions(QObject): """Container for patch queue management actions""" def __init__(self, parent=None): super(QueueManagementActions, self).__init__(parent) assert parent is None or isinstance(parent, QWidget) self._repoagent = None self._cmdsession = cmdcore.nullCmdSession() self._actions = { 'commitQueue': QAction(_('&Commit to Queue...'), self), 'createQueue': QAction(_('Create &New Queue...'), self), 'renameQueue': QAction(_('&Rename Active Queue...'), self), 'deleteQueue': QAction(_('&Delete Queue...'), self), 'purgeQueue': QAction(_('&Purge Queue...'), self), } for name, action in self._actions.iteritems(): action.triggered.connect(getattr(self, '_' + name)) self._updateActions() def setRepoAgent(self, repoagent): self._repoagent = repoagent self._updateActions() def _updateActions(self): enabled = bool(self._repoagent) and self._cmdsession.isFinished() for action in self._actions.itervalues(): action.setEnabled(enabled) def createMenu(self, parent=None): menu = QMenu(parent) menu.addAction(self._actions['commitQueue']) menu.addSeparator() for name in ['createQueue', 'renameQueue', 'deleteQueue', 'purgeQueue']: menu.addAction(self._actions[name]) return menu @pyqtSlot() def _commitQueue(self): assert self._repoagent repo = self._repoagent.rawRepo() if os.path.isdir(repo.mq.join('.hg')): self._launchCommitDialog() return if not self._cmdsession.isFinished(): return cmdline = hglib.buildcmdargs('init', mq=True) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onQueueRepoInitialized) self._updateActions() @pyqtSlot(int) def _onQueueRepoInitialized(self, ret): if ret == 0: self._launchCommitDialog() self._onCommandFinished(ret) def _launchCommitDialog(self): if not self._repoagent: return repo = self._repoagent.rawRepo() # TODO: do not instantiate mqrepo here mqrepo = thgrepo.repository(None, repo.mq.path) repoagent = mqrepo._pyqtobj dlg = commit.CommitDialog(repoagent, [], {}, self.parent()) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def switchQueue(self, name): return self._runQqueue(None, name) @pyqtSlot() def _createQueue(self): name = self._getNewName(_('Create Patch Queue'), _('New patch queue name'), _('Create')) if name: self._runQqueue('create', name) @pyqtSlot() def _renameQueue(self): curname = self._activeName() newname = self._getNewName(_('Rename Patch Queue'), _("Rename patch queue '%s' to") % curname, _('Rename')) if newname and curname != newname: self._runQqueue('rename', newname) @pyqtSlot() def _deleteQueue(self): name = self._getExistingName(_('Delete Patch Queue'), _('Delete reference to'), _('Delete')) if name: self._runQqueueInactive('delete', name) @pyqtSlot() def _purgeQueue(self): name = self._getExistingName(_('Purge Patch Queue'), _('Remove patch directory of'), _('Purge')) if name: self._runQqueueInactive('purge', name) def _activeName(self): assert self._repoagent repo = self._repoagent.rawRepo() return hglib.tounicode(repo.thgactivemqname) def _existingNames(self): assert self._repoagent return mqutil.getQQueues(self._repoagent.rawRepo()) def _getNewName(self, title, labeltext, oktext): dlg = QInputDialog(self.parent()) dlg.setWindowTitle(title) dlg.setLabelText(labeltext) dlg.setOkButtonText(oktext) if dlg.exec_(): return dlg.textValue() def _getExistingName(self, title, labeltext, oktext): dlg = QInputDialog(self.parent()) dlg.setWindowTitle(title) dlg.setLabelText(labeltext) dlg.setOkButtonText(oktext) dlg.setComboBoxEditable(False) dlg.setComboBoxItems(self._existingNames()) dlg.setTextValue(self._activeName()) if dlg.exec_(): return dlg.textValue() def abort(self): self._cmdsession.abort() def _runQqueue(self, op, name): """Execute qqueue operation against the specified queue""" assert self._repoagent if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() opts = {} if op: opts[op] = True cmdline = hglib.buildcmdargs('qqueue', name, **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onCommandFinished) self._updateActions() return sess def _runQqueueInactive(self, op, name): """Execute qqueue operation after inactivating the specified queue""" assert self._repoagent if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() if name != self._activeName(): return self._runQqueue(op, name) sacrifices = [n for n in self._existingNames() if n != name] if not sacrifices: return self._runQqueue(op, name) # will exit with error opts = {} if op: opts[op] = True cmdlines = [hglib.buildcmdargs('qqueue', sacrifices[0]), hglib.buildcmdargs('qqueue', name, **opts)] self._cmdsession = sess = self._repoagent.runCommandSequence(cmdlines, self) sess.commandFinished.connect(self._onCommandFinished) self._updateActions() return sess @pyqtSlot(int) def _onCommandFinished(self, ret): if ret != 0: cmdui.errorMessageBox(self._cmdsession, self.parent()) self._updateActions() class PatchQueueActions(QObject): """Container for MQ patch actions except for queue management""" def __init__(self, parent=None): super(PatchQueueActions, self).__init__(parent) assert parent is None or isinstance(parent, QWidget) self._repoagent = None self._cmdsession = cmdcore.nullCmdSession() self._opts = {'force': False, 'keep_changes': False} def setRepoAgent(self, repoagent): self._repoagent = repoagent def gotoPatch(self, patch): opts = {'force': self._opts['force'], 'keep_changes': self._opts['keep_changes']} return self._runCommand('qgoto', [patch], opts, self._onPushFinished) @pyqtSlot() def pushPatch(self, patch=None, move=False, exact=False): return self._runPush(patch, move=move, exact=exact) @pyqtSlot() def pushAllPatches(self): return self._runPush(None, all=True) def _runPush(self, patch, **opts): opts['force'] = self._opts['force'] if not opts.get('exact'): # --exact and --keep-changes cannot be used simultaneously # thus we ignore the "default" setting for --keep-changes # when --exact is explicitly set opts['keep_changes'] = self._opts['keep_changes'] return self._runCommand('qpush', [patch], opts, self._onPushFinished) @pyqtSlot() def popPatch(self, patch=None): return self._runPop(patch) @pyqtSlot() def popAllPatches(self): return self._runPop(None, all=True) def _runPop(self, patch, **opts): opts['force'] = self._opts['force'] opts['keep_changes'] = self._opts['keep_changes'] return self._runCommand('qpop', [patch], opts) def deletePatches(self, patches): dlg = qdelete.QDeleteDialog(patches, self.parent()) if not dlg.exec_(): return cmdcore.nullCmdSession() return self._runCommand('qdelete', patches, dlg.options()) def foldPatches(self, patches): lpatches = map(hglib.fromunicode, patches) dlg = qfold.QFoldDialog(self._repoagent, lpatches, self.parent()) dlg.finished.connect(dlg.deleteLater) if not dlg.exec_(): return cmdcore.nullCmdSession() return self._runCommand('qfold', dlg.patches(), dlg.options()) def renamePatch(self, patch): newname = patch while True: newname = self._getNewName(_('Rename Patch'), _('Rename patch %s to:') % patch, newname, _('Rename')) if not newname or patch == newname: return cmdcore.nullCmdSession() repo = self._repoagent.rawRepo() newfilename = hglib.tounicode( repo.mq.join(hglib.fromunicode(newname))) ok = qrename.checkPatchname(newfilename, self.parent()) if ok: break return self._runCommand('qrename', [patch, newname], {}) def guardPatch(self, patch, guards): args = [patch] args.extend(guards) opts = {'none': not guards} return self._runCommand('qguard', args, opts) def selectGuards(self, guards): opts = {'none': not guards} return self._runCommand('qselect', guards, opts) def _getNewName(self, title, labeltext, curvalue, oktext): dlg = QInputDialog(self.parent()) dlg.setWindowTitle(title) dlg.setLabelText(labeltext) dlg.setTextValue(curvalue) dlg.setOkButtonText(oktext) if dlg.exec_(): return unicode(dlg.textValue()) def abort(self): self._cmdsession.abort() def _runCommand(self, name, args, opts, finishslot=None): assert self._repoagent if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() cmdline = hglib.buildcmdargs(name, *args, **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(finishslot or self._onCommandFinished) return sess @pyqtSlot(int) def _onPushFinished(self, ret): if ret != 0 and self._repoagent: repo = self._repoagent.rawRepo() output = hglib.fromunicode(self._cmdsession.warningString()) if mqutil.checkForRejects(repo, output, self.parent()) > 0: ret = 0 # no further error dialog if ret != 0: cmdui.errorMessageBox(self._cmdsession, self.parent()) @pyqtSlot(int) def _onCommandFinished(self, ret): if ret != 0: cmdui.errorMessageBox(self._cmdsession, self.parent()) @pyqtSlot() def launchOptionsDialog(self): dlg = OptionsDialog(self._opts, self.parent()) dlg.finished.connect(dlg.deleteLater) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self._opts.update(dlg.outopts) class PatchQueueModel(QAbstractListModel): """List of all patches in active queue""" def __init__(self, repoagent, parent=None): super(PatchQueueModel, self).__init__(parent) self._repoagent = repoagent self._repoagent.repositoryChanged.connect(self._updateCache) self._series = [] self._seriesguards = [] self._statusmap = {} # patch: applied/guarded/unguarded self._buildCache() @pyqtSlot() def _updateCache(self): # optimize range of changed signals if necessary repo = self._repoagent.rawRepo() if self._series == repo.mq.series[::-1]: self._buildCache() else: self._updateCacheAndLayout() self.dataChanged.emit(self.index(0), self.index(self.rowCount() - 1)) def _updateCacheAndLayout(self): self.layoutAboutToBeChanged.emit() oldindexes = [(oi, self._series[oi.row()]) for oi in self.persistentIndexList()] self._buildCache() for oi, patch in oldindexes: try: ni = self.index(self._series.index(patch), oi.column()) except ValueError: ni = QModelIndex() self.changePersistentIndex(oi, ni) self.layoutChanged.emit() def _buildCache(self): repo = self._repoagent.rawRepo() self._series = repo.mq.series[::-1] self._seriesguards = [list(xs) for xs in reversed(repo.mq.seriesguards)] self._statusmap.clear() self._statusmap.update((p.name, 'applied') for p in repo.mq.applied) for i, patch in enumerate(repo.mq.series): if patch in self._statusmap: continue # applied pushable, why = repo.mq.pushable(i) if not pushable: self._statusmap[patch] = 'guarded' elif why is not None: self._statusmap[patch] = 'unguarded' def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return if role in (Qt.DisplayRole, Qt.EditRole): return self.patchName(index) if role == Qt.DecorationRole: return self._statusIcon(index) if role == Qt.FontRole: return self._statusFont(index) if role == Qt.ToolTipRole: return self._toolTip(index) def flags(self, index): flags = super(PatchQueueModel, self).flags(index) if not index.isValid(): return flags | Qt.ItemIsDropEnabled # insertion point patch = self._series[index.row()] if self._statusmap.get(patch) != 'applied': flags |= Qt.ItemIsDragEnabled return flags def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 return len(self._series) def appliedCount(self): return sum(s == 'applied' for s in self._statusmap.itervalues()) def patchName(self, index): if not index.isValid(): return '' return hglib.tounicode(self._series[index.row()]) def patchGuards(self, index): if not index.isValid(): return [] return map(hglib.tounicode, self._seriesguards[index.row()]) def isApplied(self, index): if not index.isValid(): return False patch = self._series[index.row()] return self._statusmap.get(patch) == 'applied' def _statusIcon(self, index): assert index.isValid() patch = self._series[index.row()] status = self._statusmap.get(patch) if status: return qtlib.geticon('hg-patch-%s' % status) def _statusFont(self, index): assert index.isValid() patch = self._series[index.row()] status = self._statusmap.get(patch) if status not in ('applied', 'guarded'): return f = QFont() f.setBold(status == 'applied') f.setItalic(status == 'guarded') return f def _toolTip(self, index): assert index.isValid() repo = self._repoagent.rawRepo() patch = self._series[index.row()] try: ctx = repo.changectx(patch) except error.RepoLookupError: # cache not updated after qdelete or qfinish return guards = self.patchGuards(index) return '%s: %s\n%s' % (self.patchName(index), guards and ', '.join(guards) or _('no guards'), ctx.longsummary()) def mimeTypes(self): return ['application/vnd.thg.mq.series', 'text/uri-list'] def mimeData(self, indexes): repo = self._repoagent.rawRepo() # in the same order as series file patches = [self._series[i.row()] for i in sorted(indexes, reverse=True)] data = QMimeData() data.setData('application/vnd.thg.mq.series', QByteArray('\n'.join(patches) + '\n')) data.setUrls([QUrl.fromLocalFile(hglib.tounicode(repo.mq.join(p))) for p in patches]) return data def dropMimeData(self, data, action, row, column, parent): if (action != Qt.MoveAction or not data.hasFormat('application/vnd.thg.mq.series') or row < 0 or parent.isValid()): return False repo = self._repoagent.rawRepo() qtiprow = len(self._series) - repo.mq.seriesend(True) if row > qtiprow: return False if row < len(self._series): after = self._series[row] else: after = None # next to working rev patches = str(data.data('application/vnd.thg.mq.series')).splitlines() if hglib.movemqpatches(repo, after, patches): self._repoagent.pollStatus() return True def supportedDropActions(self): return Qt.MoveAction class MQPatchesWidget(QDockWidget): patchSelected = pyqtSignal(unicode) def __init__(self, parent): QDockWidget.__init__(self, parent) self._repoagent = None self.setFeatures(QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.setWindowTitle(_('Patch Queue')) w = QWidget() mainlayout = QVBoxLayout() mainlayout.setContentsMargins(0, 0, 0, 0) w.setLayout(mainlayout) self.setWidget(w) self.patchActions = PatchQueueActions(self) # top toolbar w = QWidget() tbarhbox = QHBoxLayout() tbarhbox.setContentsMargins(0, 0, 0, 0) w.setLayout(tbarhbox) mainlayout.addWidget(w) # TODO: move QAction instances to PatchQueueActions self.qpushAllAct = a = QAction( geticon('hg-qpush-all'), _('Push all', 'MQ QPush'), self) a.setToolTip(_('Apply all patches')) self.qpushAct = a = QAction( geticon('hg-qpush'), _('Push', 'MQ QPush'), self) a.setToolTip(_('Apply one patch')) self.setGuardsAct = a = QAction( geticon('hg-qguard'), _('Set &Guards...'), self) a.setToolTip(_('Configure guards for selected patch')) self.qdeleteAct = a = QAction( geticon('hg-qdelete'), _('&Delete Patches...'), self) a.setToolTip(_('Delete selected patches')) self.qpopAct = a = QAction( geticon('hg-qpop'), _('Pop'), self) a.setToolTip(_('Unapply one patch')) self.qpopAllAct = a = QAction( geticon('hg-qpop-all'), _('Pop all'), self) a.setToolTip(_('Unapply all patches')) self.qrenameAct = QAction(_('Re&name Patch...'), self) self.qtbar = tbar = QToolBar(_('Patch Queue Actions Toolbar')) tbar.setIconSize(QSize(18, 18)) tbarhbox.addWidget(tbar) tbar.addAction(self.qpushAct) tbar.addAction(self.qpushAllAct) tbar.addSeparator() tbar.addAction(self.qpopAct) tbar.addAction(self.qpopAllAct) tbar.addSeparator() tbar.addAction(self.qdeleteAct) tbar.addSeparator() tbar.addAction(self.setGuardsAct) self.queueFrame = w = QFrame() mainlayout.addWidget(w) # Patch Queue Frame layout = QVBoxLayout() layout.setSpacing(5) layout.setContentsMargins(0, 0, 0, 0) self.queueFrame.setLayout(layout) qqueuehbox = QHBoxLayout() qqueuehbox.setSpacing(5) layout.addLayout(qqueuehbox) self.qqueueComboWidget = QComboBox(self) qqueuehbox.addWidget(self.qqueueComboWidget, 1) self.qqueueConfigBtn = QToolButton(self) self.qqueueConfigBtn.setText('...') self.qqueueConfigBtn.setPopupMode(QToolButton.InstantPopup) qqueuehbox.addWidget(self.qqueueConfigBtn) self.qqueueActions = QueueManagementActions(self) self.qqueueConfigBtn.setMenu(self.qqueueActions.createMenu(self)) self.queueListWidget = QListView(self) self.queueListWidget.setDragDropMode(QAbstractItemView.InternalMove) self.queueListWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.queueListWidget.setIconSize(QSize(12, 12)) self.queueListWidget.setSelectionMode( QAbstractItemView.ExtendedSelection) self.queueListWidget.setContextMenuPolicy(Qt.CustomContextMenu) self.queueListWidget.customContextMenuRequested.connect( self.onMenuRequested) layout.addWidget(self.queueListWidget, 1) bbarhbox = QHBoxLayout() bbarhbox.setSpacing(5) layout.addLayout(bbarhbox) self.guardSelBtn = QPushButton() menu = QMenu(self) menu.triggered.connect(self.onGuardSelectionChange) self.guardSelBtn.setMenu(menu) bbarhbox.addWidget(self.guardSelBtn) self.qqueueComboWidget.activated[QString].connect( self.onQQueueActivated) self.queueListWidget.activated.connect(self.onGotoPatch) self.qpushAllAct.triggered.connect(self.patchActions.pushAllPatches) self.qpushAct.triggered[()].connect(self.patchActions.pushPatch) self.qpopAllAct.triggered.connect(self.patchActions.popAllPatches) self.qpopAct.triggered[()].connect(self.patchActions.popPatch) self.setGuardsAct.triggered.connect(self.onGuardConfigure) self.qdeleteAct.triggered.connect(self.onDelete) self.qrenameAct.triggered.connect(self.onRenamePatch) self.setAcceptDrops(True) self.layout().setContentsMargins(2, 2, 2, 2) QTimer.singleShot(0, self.reload) @property def repo(self): if self._repoagent: return self._repoagent.rawRepo() def setRepoAgent(self, repoagent): if self._repoagent: self._repoagent.repositoryChanged.disconnect(self.reload) self._repoagent = None if repoagent and 'mq' in repoagent.rawRepo().extensions(): self._repoagent = repoagent self._repoagent.repositoryChanged.connect(self.reload) self._changePatchQueueModel() self.patchActions.setRepoAgent(repoagent) self.qqueueActions.setRepoAgent(repoagent) QTimer.singleShot(0, self.reload) def _changePatchQueueModel(self): oldmodel = self.queueListWidget.model() if self._repoagent: newmodel = PatchQueueModel(self._repoagent, self) self.queueListWidget.setModel(newmodel) newmodel.dataChanged.connect(self._updatePatchActions) selmodel = self.queueListWidget.selectionModel() selmodel.currentRowChanged.connect(self.onPatchSelected) selmodel.selectionChanged.connect(self._updatePatchActions) self._updatePatchActions() else: self.queueListWidget.setModel(None) if oldmodel: oldmodel.setParent(None) @pyqtSlot() def showActiveQueue(self): combo = self.qqueueComboWidget q = hglib.tounicode(self.repo.thgactivemqname) index = combo.findText(q) combo.setCurrentIndex(index) @pyqtSlot(QPoint) def onMenuRequested(self, pos): menu = QMenu(self) menu.addAction(self.qdeleteAct) menu.addAction(self.qrenameAct) menu.addAction(self.setGuardsAct) menu.exec_(self.queueListWidget.viewport().mapToGlobal(pos)) menu.setParent(None) @pyqtSlot() def onGuardConfigure(self): model = self.queueListWidget.model() index = self.queueListWidget.currentIndex() patch = model.patchName(index) uguards = ' '.join(model.patchGuards(index)) new, ok = qtlib.getTextInput(self, _('Configure guards'), _('Input new guards for %s:') % patch, text=uguards) if not ok or new == uguards: return self.patchActions.guardPatch(patch, unicode(new).split()) @pyqtSlot() def onDelete(self): model = self.queueListWidget.model() selmodel = self.queueListWidget.selectionModel() patches = map(model.patchName, selmodel.selectedRows()) self.patchActions.deletePatches(patches) #@pyqtSlot(QModelIndex) def onGotoPatch(self, index): 'Patch has been activated (return), issue qgoto' patch = self.queueListWidget.model().patchName(index) self.patchActions.gotoPatch(patch) @pyqtSlot() def onRenamePatch(self): index = self.queueListWidget.currentIndex() patch = self.queueListWidget.model().patchName(index) self.patchActions.renamePatch(patch) #@pyqtSlot(QModelIndex) def onPatchSelected(self, index): if index.isValid(): model = self.queueListWidget.model() self.patchSelected.emit(model.patchName(index)) @pyqtSlot() def _updatePatchActions(self): model = self.queueListWidget.model() selmodel = self.queueListWidget.selectionModel() appliedcnt = model.appliedCount() seriescnt = model.rowCount() self.qpushAllAct.setEnabled(seriescnt > appliedcnt) self.qpushAct.setEnabled(seriescnt > appliedcnt) self.qpopAct.setEnabled(appliedcnt > 0) self.qpopAllAct.setEnabled(appliedcnt > 0) indexes = selmodel.selectedRows() anyapplied = util.any(model.isApplied(i) for i in indexes) self.qdeleteAct.setEnabled(len(indexes) > 0 and not anyapplied) self.setGuardsAct.setEnabled(len(indexes) == 1) self.qrenameAct.setEnabled(len(indexes) == 1) @pyqtSlot(QString) def onQQueueActivated(self, text): if text == hglib.tounicode(self.repo.thgactivemqname): return if qtlib.QuestionMsgBox(_('Confirm patch queue switch'), _("Do you really want to activate patch queue '%s' ?") % text, parent=self, defaultbutton=QMessageBox.No): sess = self.qqueueActions.switchQueue(text) sess.commandFinished.connect(self.showActiveQueue) else: self.showActiveQueue() @pyqtSlot() def reload(self): self.widget().setEnabled(bool(self._repoagent)) if not self._repoagent: return self.loadQQueues() self.showActiveQueue() repo = self.repo self.allguards = set() for idx, patch in enumerate(repo.mq.series): patchguards = repo.mq.seriesguards[idx] if patchguards: for guard in patchguards: self.allguards.add(guard[1:]) for guard in repo.mq.active(): self.allguards.add(guard) self.refreshSelectedGuards() self.qqueueComboWidget.setEnabled(self.qqueueComboWidget.count() > 1) def loadQQueues(self): repo = self.repo combo = self.qqueueComboWidget combo.clear() combo.addItems(mqutil.getQQueues(repo)) def refreshSelectedGuards(self): total = len(self.allguards) count = len(self.repo.mq.active()) menu = self.guardSelBtn.menu() menu.clear() for guard in self.allguards: a = menu.addAction(hglib.tounicode(guard)) a.setCheckable(True) a.setChecked(guard in self.repo.mq.active()) self.guardSelBtn.setText(_('Guards: %d/%d') % (count, total)) self.guardSelBtn.setEnabled(bool(total)) @pyqtSlot(QAction) def onGuardSelectionChange(self, action): guard = hglib.fromunicode(action.text()) newguards = self.repo.mq.active()[:] if action.isChecked(): newguards.append(guard) elif guard in newguards: newguards.remove(guard) self.patchActions.selectGuards(map(hglib.tounicode, newguards)) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.patchActions.abort() self.qqueueActions.abort() else: return super(MQPatchesWidget, self).keyPressEvent(event) class OptionsDialog(QDialog): 'Utility dialog for configuring uncommon options' def __init__(self, opts, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('MQ options')) layout = QVBoxLayout() self.setLayout(layout) self.forcecb = QCheckBox( _('Force push or pop (--force)')) layout.addWidget(self.forcecb) self.keepcb = QCheckBox( _('Tolerate non-conflicting local changes (--keep-changes)')) layout.addWidget(self.keepcb) self.forcecb.setChecked(opts.get('force', False)) self.keepcb.setChecked(opts.get('keep_changes', False)) for cb in [self.forcecb, self.keepcb]: cb.clicked.connect(self._resolveopts) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.bb = bb layout.addWidget(bb) #@pyqtSlot() def _resolveopts(self): # cannot use both --force and --keep-changes exclmap = {self.forcecb: [self.keepcb], self.keepcb: [self.forcecb], } sendercb = self.sender() if sendercb.isChecked(): for cb in exclmap[sendercb]: cb.setChecked(False) def accept(self): outopts = {} outopts['force'] = self.forcecb.isChecked() outopts['keep_changes'] = self.keepcb.isChecked() self.outopts = outopts QDialog.accept(self) tortoisehg-2.10/tortoisehg/hgqt/filelistview.py0000644000076400007640000001230412170335562021107 0ustar stevesteve# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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 os from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, visdiff from PyQt4.QtCore import * from PyQt4.QtGui import * class HgFileListView(QTableView): """ A QTableView for displaying a HgFileListModel """ fileSelected = pyqtSignal(QString, QString) clearDisplay = pyqtSignal() def __init__(self, repo, parent, multiselectable): QTableView.__init__(self, parent) self.repo = repo self.multiselectable = multiselectable self.setShowGrid(False) self.horizontalHeader().hide() self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(20) if multiselectable: self.setSelectionMode(QAbstractItemView.ExtendedSelection) else: self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setTextElideMode(Qt.ElideLeft) self._paletteswitcher = qtlib.PaletteSwitcher(self) def setModel(self, model): QTableView.setModel(self, model) model.layoutChanged.connect(self.layoutChanged) self.selectionModel().currentRowChanged.connect(self.onRowChange) def setRepo(self, repo): self.repo = repo def setContext(self, ctx): self.ctx = ctx self.model().setContext(ctx) def currentFile(self): index = self.currentIndex() return self.model().fileFromIndex(index) def getSelectedFiles(self): model = self.model() sf = [model.fileFromIndex(eachIndex) for eachIndex in self.selectedRows()] return sf def layoutChanged(self): 'file model has new contents' index = self.currentIndex() count = len(self.model()) if index.row() == -1: # index is changing, onRowChange() called for us self.selectRow(0) elif index.row() >= count: if count: # index is changing, onRowChange() called for us self.selectRow(count-1) else: self.clearDisplay.emit() else: # redisplay previous row self.onRowChange(index) def onRowChange(self, index, *args): if index is None: index = self.currentIndex() data = self.model().dataFromIndex(index) if data: self.fileSelected.emit(hglib.tounicode(data['path']), data['status']) else: self.clearDisplay.emit() def resizeEvent(self, event): if self.model() is not None: vp_width = self.viewport().width() col_widths = [self.columnWidth(i) \ for i in range(1, self.model().columnCount())] col_width = vp_width - sum(col_widths) col_width = max(col_width, 50) self.setColumnWidth(0, col_width) QTableView.resizeEvent(self, event) def enablefilterpalette(self, enable): self._paletteswitcher.enablefilterpalette(enable) # ## Mouse drag # def selectedRows(self): return self.selectionModel().selectedRows() def dragObject(self): if type(self.ctx.rev()) == str: return paths = [] for index in self.selectedRows(): paths.append(self.model().fileFromIndex(index)) if not paths: return if self.ctx.rev() is None: base = self.repo.root else: base, _ = visdiff.snapshot(self.repo, paths, self.ctx) urls = [] for path in paths: urls.append(QUrl.fromLocalFile(os.path.join(base, path))) if urls: d = QDrag(self) m = QMimeData() m.setUrls(urls) d.setMimeData(m) d.start(Qt.CopyAction) def mousePressEvent(self, event): self.pressPos = event.pos() self.pressTime = QTime.currentTime() return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): d = event.pos() - self.pressPos if d.manhattanLength() < QApplication.startDragDistance(): return QTableView.mouseMoveEvent(self, event) elapsed = self.pressTime.msecsTo(QTime.currentTime()) if elapsed < QApplication.startDragTime(): return QTableView.mouseMoveEvent(self, event) self.dragObject() return QTableView.mouseMoveEvent(self, event) tortoisehg-2.10/tortoisehg/hgqt/resolve.py0000644000076400007640000004700212231647662020070 0ustar stevesteve# resolve.py - TortoiseHg merge conflict resolve # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * import os from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, cmdui, csinfo, visdiff, thgrepo MARGINS = (8, 0, 0, 0) class ResolveDialog(QDialog): def __init__(self, repoagent, parent=None): super(ResolveDialog, self).__init__(parent) self._repoagent = repoagent repo = repoagent.rawRepo() self.setWindowFlags(Qt.Window) self.setWindowTitle(_('Resolve Conflicts - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-merge')) self.setLayout(QVBoxLayout()) self.layout().setSpacing(5) hbox = QHBoxLayout() self.layout().addLayout(hbox) self.refreshButton = tb = QToolButton(self) tb.setIcon(qtlib.geticon('view-refresh')) tb.setShortcut(QKeySequence.Refresh) tb.clicked.connect(self.refresh) self.stlabel = QLabel() hbox.addWidget(tb) hbox.addWidget(self.stlabel) def revisionInfoLayout(repo): """ Return a layout containg the revision information (local and other) """ hbox = QHBoxLayout() hbox.setSpacing(0) hbox.setContentsMargins(*MARGINS) vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) localrevtitle = qtlib.LabeledSeparator(_('Local revision information')) localrevinfo = csinfo.create(repo) localrevinfo.update(repo[None].p1()) vbox.addWidget(localrevtitle) vbox.addWidget(localrevinfo) vbox.addStretch() vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) otherrevtitle = qtlib.LabeledSeparator(_('Other revision information')) otherrevinfo = csinfo.create(repo) otherrevinfo.update(repo[None].p2()) vbox.addWidget(otherrevtitle) vbox.addWidget(otherrevinfo) vbox.addStretch() return hbox if len(self.repo[None].parents()) > 1: self.layout().addLayout(revisionInfoLayout(self.repo)) unres = qtlib.LabeledSeparator(_('Unresolved conflicts')) self.layout().addWidget(unres) hbox = QHBoxLayout() hbox.setSpacing(0) hbox.setContentsMargins(*MARGINS) self.layout().addLayout(hbox) self.utree = PathsTree(self.repo, self) hbox.addWidget(self.utree) vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) auto = QPushButton(_('Mercurial Re&solve')) auto.setToolTip(_('Attempt automatic (trivial) merge')) auto.clicked.connect(lambda: self.merge('internal:merge')) manual = QPushButton(_('Tool &Resolve')) manual.setToolTip(_('Merge using selected merge tool')) manual.clicked.connect(self.merge) local = QPushButton(_('&Take Local')) local.setToolTip(_('Accept the local file version (yours)')) local.clicked.connect(lambda: self.merge('internal:local')) other = QPushButton(_('Take &Other')) other.setToolTip(_('Accept the other file version (theirs)')) other.clicked.connect(lambda: self.merge('internal:other')) res = QPushButton(_('&Mark as Resolved')) res.setToolTip(_('Mark this file as resolved')) res.clicked.connect(self.markresolved) vbox.addWidget(auto) vbox.addWidget(manual) vbox.addWidget(local) vbox.addWidget(other) vbox.addWidget(res) vbox.addStretch(1) self.ubuttons = (auto, manual, local, other, res) self.utree.setContextMenuPolicy(Qt.CustomContextMenu) self.utreecmenu = QMenu(self) cmauto = self.utreecmenu.addAction(_('Mercurial Re&solve')) cmauto.triggered.connect(lambda: self.merge('internal:merge')) cmmanual = self.utreecmenu.addAction(_('Tool &Resolve')) cmmanual.triggered.connect(self.merge) cmlocal = self.utreecmenu.addAction(_('&Take Local')) cmlocal.triggered.connect(lambda: self.merge('internal:local')) cmother = self.utreecmenu.addAction(_('Take &Other')) cmother.triggered.connect(lambda: self.merge('internal:other')) cmres = self.utreecmenu.addAction(_('&Mark as Resolved')) cmres.triggered.connect(self.markresolved) self.utreecmenu.addSeparator() cmdiffLocToAnc = self.utreecmenu.addAction(_('Diff &Local to Ancestor')) cmdiffLocToAnc.triggered.connect(self.diffLocToAnc) cmdiffOthToAnc = self.utreecmenu.addAction(_('&Diff Other to Ancestor')) cmdiffOthToAnc.triggered.connect(self.diffOthToAnc) self.umenuitems = (cmauto, cmmanual, cmlocal, cmother, cmres, cmdiffLocToAnc, cmdiffOthToAnc) self.utree.customContextMenuRequested.connect(self.utreeMenuRequested) self.utree.doubleClicked.connect(self.utreeDoubleClicked) res = qtlib.LabeledSeparator(_('Resolved conflicts')) self.layout().addWidget(res) hbox = QHBoxLayout() hbox.setContentsMargins(*MARGINS) hbox.setSpacing(0) self.layout().addLayout(hbox) self.rtree = PathsTree(self.repo, self) hbox.addWidget(self.rtree) vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) edit = QPushButton(_('&Edit File')) edit.setToolTip(_('Edit resolved file')) edit.clicked.connect(self.edit) v3way = QPushButton(_('3-&Way Diff')) v3way.setToolTip(_('Visual three-way diff')) v3way.clicked.connect(self.v3way) vp0 = QPushButton(_('Diff to &Local')) vp0.setToolTip(_('Visual diff between resolved file and first parent')) vp0.clicked.connect(self.vp0) vp1 = QPushButton(_('&Diff to Other')) vp1.setToolTip(_('Visual diff between resolved file and second parent')) vp1.clicked.connect(self.vp1) ures = QPushButton(_('Mark as &Unresolved')) ures.setToolTip(_('Mark this file as unresolved')) ures.clicked.connect(self.markunresolved) vbox.addWidget(edit) vbox.addWidget(v3way) vbox.addWidget(vp0) vbox.addWidget(vp1) vbox.addWidget(ures) vbox.addStretch(1) self.rbuttons = (edit, vp0, ures) self.rmbuttons = (vp1, v3way) self.rtree.setContextMenuPolicy(Qt.CustomContextMenu) self.rtreecmenu = QMenu(self) cmedit = self.rtreecmenu.addAction(_('&Edit File')) cmedit.triggered.connect(self.edit) cmv3way = self.rtreecmenu.addAction(_('3-&Way Diff')) cmv3way.triggered.connect(self.v3way) cmvp0 = self.rtreecmenu.addAction(_('Diff to &Local')) cmvp0.triggered.connect(self.vp0) cmvp1 = self.rtreecmenu.addAction(_('&Diff to Other')) cmvp1.triggered.connect(self.vp1) cmures = self.rtreecmenu.addAction(_('Mark as &Unresolved')) cmures.triggered.connect(self.markunresolved) self.rmenuitems = (cmedit, cmvp0, cmures) self.rmmenuitems = (cmvp1, cmv3way) self.rtree.customContextMenuRequested.connect(self.rtreeMenuRequested) self.rtree.doubleClicked.connect(self.v3way) hbox = QHBoxLayout() hbox.setContentsMargins(*MARGINS) hbox.setSpacing(4) self.layout().addLayout(hbox) self.tcombo = ToolsCombo(self.repo, self) hbox.addWidget(QLabel(_('Detected merge/diff tools:'))) hbox.addWidget(self.tcombo) hbox.addStretch(1) out = qtlib.LabeledSeparator(_('Command output')) self.layout().addWidget(out) self.cmd = cmdui.Widget(True, False, self) self.cmd.commandFinished.connect(self.refresh) self.cmd.setShowOutput(True) self.layout().addWidget(self.cmd) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Close) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) self.bbox = bbox s = QSettings() self.restoreGeometry(s.value('resolve/geom').toByteArray()) self.refresh() self.utree.selectAll() self.utree.setFocus() repoagent.configChanged.connect(self.configChanged) repoagent.repositoryChanged.connect(self.repositoryChanged) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def repositoryChanged(self): self.refresh() def getSelectedPaths(self, tree): paths = [] repo = self.repo if not tree.selectionModel(): return paths for idx in tree.selectionModel().selectedRows(): root, wfile = tree.model().getPathForIndex(idx) paths.append((root, wfile)) return paths def runCommand(self, tree, cmdline): cmdlines = [] selected = self.getSelectedPaths(tree) while selected: curroot = selected[0][0] cmd = cmdline + ['--repository', curroot, '--'] for root, wfile in selected: if root == curroot: cmd.append(os.path.normpath(os.path.join(root, wfile))) cmdlines.append(cmd) selected = [(r, w) for r, w in selected if r != curroot] if cmdlines: self.cmd.run(*cmdlines) def merge(self, tool=False): if not tool: tool = self.tcombo.readValue() cmd = ['resolve'] if tool: cmd += ['--tool='+tool] self.runCommand(self.utree, cmd) def markresolved(self): self.runCommand(self.utree, ['resolve', '--mark']) def markunresolved(self): self.runCommand(self.rtree, ['resolve', '--unmark']) def edit(self): paths = self.getSelectedPaths(self.rtree) if paths: abspaths = [os.path.join(r,w) for r,w in paths] qtlib.editfiles(self.repo, abspaths, parent=self) def getVdiffFiles(self, tree): paths = self.getSelectedPaths(tree) if not paths: return [] files, sub = [], False for root, wfile in paths: if root == self.repo.root: files.append(wfile) else: sub = True if sub: qtlib.InfoMsgBox(_('Unable to show subrepository files'), _('Visual diffs are not supported for files in ' 'subrepositories. They will not be shown.')) return files def v3way(self): paths = self.getVdiffFiles(self.rtree) if paths: opts = {} opts['rev'] = [] opts['tool'] = self.tcombo.readValue() dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def vp0(self): paths = self.getVdiffFiles(self.rtree) if paths: opts = {} opts['rev'] = ['p1()'] opts['tool'] = self.tcombo.readValue() dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def vp1(self): paths = self.getVdiffFiles(self.rtree) if paths: opts = {} opts['rev'] = ['p2()'] opts['tool'] = self.tcombo.readValue() dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def diffLocToAnc(self): paths = self.getVdiffFiles(self.utree) if paths: opts = {} opts['rev'] = ['ancestor(p1(),p2())..p1()'] opts['tool'] = self.tcombo.readValue() dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def diffOthToAnc(self): paths = self.getVdiffFiles(self.utree) if paths: opts = {} opts['rev'] = ['ancestor(p1(),p2())..p2()'] opts['tool'] = self.tcombo.readValue() dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() @pyqtSlot() def configChanged(self): 'repository has detected a change to config files' self.tcombo.reset() def refresh(self): repo = self.repo u, r = [], [] for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': u.append((root, path)) else: r.append((root, path)) paths = self.getSelectedPaths(self.utree) oldmodel = self.utree.model() self.utree.setModel(PathsModel(u, self)) self.utree.resizeColumnToContents(0) self.utree.resizeColumnToContents(1) if oldmodel: oldmodel.setParent(None) # gc-ed model = self.utree.model() smodel = self.utree.selectionModel() sflags = QItemSelectionModel.Select | QItemSelectionModel.Columns for i, path in enumerate(u): if path in paths: smodel.select(model.index(i, 0), sflags) smodel.select(model.index(i, 1), sflags) smodel.select(model.index(i, 2), sflags) @pyqtSlot(QItemSelection, QItemSelection) def uchanged(selected, deselected): enable = self.utree.selectionModel().hasSelection() for b in self.ubuttons: b.setEnabled(enable) for c in self.umenuitems: c.setEnabled(enable) smodel.selectionChanged.connect(uchanged) uchanged(None, None) paths = self.getSelectedPaths(self.rtree) oldmodel = self.rtree.model() self.rtree.setModel(PathsModel(r, self)) self.rtree.resizeColumnToContents(0) self.rtree.resizeColumnToContents(1) if oldmodel: oldmodel.setParent(None) # gc-ed model = self.rtree.model() smodel = self.rtree.selectionModel() for i, path in enumerate(r): if path in paths: smodel.select(model.index(i, 0), sflags) smodel.select(model.index(i, 1), sflags) smodel.select(model.index(i, 2), sflags) @pyqtSlot(QItemSelection, QItemSelection) def rchanged(selected, deselected): enable = self.rtree.selectionModel().hasSelection() for b in self.rbuttons: b.setEnabled(enable) for c in self.rmenuitems: c.setEnabled(enable) merge = len(self.repo.parents()) > 1 for b in self.rmbuttons: b.setEnabled(enable and merge) for c in self.rmmenuitems: c.setEnabled(enable and merge) smodel.selectionChanged.connect(rchanged) rchanged(None, None) if u: txt = _('There are merge conflicts to be resolved') elif r: txt = _('All conflicts are resolved.') else: txt = _('There are no conflicting file merges.') self.stlabel.setText(u'

' + txt + u'

') def reject(self): s = QSettings() s.setValue('resolve/geom', self.saveGeometry()) if self.utree.model().rowCount() > 0: main = _('Exit without finishing resolve?') text = _('Unresolved conflicts remain. Are you sure?') labels = ((QMessageBox.Yes, _('E&xit')), (QMessageBox.No, _('Cancel'))) if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, labels=labels, parent=self): return super(ResolveDialog, self).reject() @pyqtSlot(QPoint) def utreeMenuRequested(self, point): self.utreecmenu.exec_(self.utree.viewport().mapToGlobal(point)) @pyqtSlot(QPoint) def rtreeMenuRequested(self, point): self.rtreecmenu.exec_(self.rtree.viewport().mapToGlobal(point)) def utreeDoubleClicked(self): if self.repo.ui.configbool('tortoisehg', 'autoresolve'): self.merge() else: self.merge('internal:merge') class PathsTree(QTreeView): def __init__(self, repo, parent): QTreeView.__init__(self, parent) self.repo = repo self.setSelectionMode(QTreeView.ExtendedSelection) self.setSortingEnabled(True) def dragObject(self): urls = [] for index in self.selectionModel().selectedRows(): root, path = self.model().getPathForIndex(index) urls.append(QUrl.fromLocalFile(os.path.join(root, path))) if urls: d = QDrag(self) m = QMimeData() m.setUrls(urls) d.setMimeData(m) d.start(Qt.CopyAction) def mousePressEvent(self, event): self.pressPos = event.pos() self.pressTime = QTime.currentTime() return QTreeView.mousePressEvent(self, event) def mouseMoveEvent(self, event): d = event.pos() - self.pressPos if d.manhattanLength() < QApplication.startDragDistance(): return QTreeView.mouseMoveEvent(self, event) elapsed = self.pressTime.msecsTo(QTime.currentTime()) if elapsed < QApplication.startDragTime(): return QTreeView.mouseMoveEvent(self, event) self.dragObject() return QTreeView.mouseMoveEvent(self, event) class PathsModel(QAbstractTableModel): def __init__(self, pathlist, parent): QAbstractTableModel.__init__(self, parent) self.headers = (_('Path'), _('Ext'), _('Repository')) self.rows = [] for root, path in pathlist: name, ext = os.path.splitext(path) self.rows.append([path, ext, root]) def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self.rows) def columnCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self.headers) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role == Qt.DisplayRole: data = self.rows[index.row()][index.column()] return QVariant(hglib.tounicode(data)) return QVariant() def headerData(self, col, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() else: return QVariant(self.headers[col]) def getPathForIndex(self, index): 'return root, wfile for the given row' row = index.row() return self.rows[row][2], self.rows[row][0] class ToolsCombo(QComboBox): def __init__(self, repo, parent): QComboBox.__init__(self, parent) self.setEditable(False) self.loaded = False self.default = _('') self.addItem(self.default) self.repo = repo def reset(self): self.loaded = False self.clear() self.addItem(self.default) def showPopup(self): if not self.loaded: self.loaded = True self.clear() self.addItem(self.default) for t in self.repo.mergetools: self.addItem(hglib.tounicode(t)) QComboBox.showPopup(self) def readValue(self): if self.loaded: text = self.currentText() if text != self.default: return hglib.fromunicode(text) else: return None tortoisehg-2.10/tortoisehg/hgqt/csinfo.py0000644000076400007640000004376112231647662017702 0ustar stevesteve# csinfo.py - An embeddable widget for changeset summary # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import re import binascii from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error from mercurial.node import hex from tortoisehg.util import hglib, paths from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, thgrepo PANEL_DEFAULT = ('rev', 'summary', 'user', 'dateage', 'branch', 'close', 'tags', 'graft', 'transplant', 'obsolete', 'p4', 'svn', 'converted',) def create(repo, target=None, style=None, custom=None, **kargs): return Factory(repo, custom, style, target, **kargs)() def factory(*args, **kargs): return Factory(*args, **kargs) def panelstyle(**kargs): kargs['type'] = 'panel' if 'contents' not in kargs: kargs['contents'] = PANEL_DEFAULT return kargs def labelstyle(**kargs): kargs['type'] = 'label' return kargs def custom(**kargs): return kargs class Factory(object): def __init__(self, repo, custom=None, style=None, target=None, withupdate=False): if repo is None: raise _('must be specified repository') self.repo = repo self.target = target if custom is None: custom = {} self.custom = custom if style is None: style = panelstyle() self.csstyle = style self.info = SummaryInfo() self.withupdate = withupdate def __call__(self, target=None, style=None, custom=None, repo=None): # try to create a context object if target is None: target = self.target if repo is None: repo = self.repo if style is None: style = self.csstyle else: # need to override styles newstyle = self.csstyle.copy() newstyle.update(style) style = newstyle if custom is None: custom = self.custom else: # need to override customs newcustom = self.custom.copy() newcustom.update(custom) custom = newcustom if 'type' not in style: raise _("must be specified 'type' in style") type = style['type'] assert type in ('panel', 'label') # create widget args = (target, style, custom, repo, self.info) if type == 'panel': widget = SummaryPanel(*args) else: widget = SummaryLabel(*args) if self.withupdate: widget.update() return widget class UnknownItem(Exception): pass class SummaryInfo(object): LABELS = {'rev': _('Revision:'), 'revnum': _('Revision:'), 'revid': _('Revision:'), 'summary': _('Summary:'), 'user': _('User:'), 'date': _('Date:'),'age': _('Age:'), 'dateage': _('Date:'), 'branch': _('Branch:'), 'close': _('Close:'), 'tags': _('Tags:'), 'rawbranch': _('Branch:'), 'graft': _('Graft:'), 'transplant': _('Transplant:'), 'obsolete': _('Obsolete state:'), 'p4': _('Perforce:'), 'svn': _('Subversion:'), 'converted': _('Converted From:'), 'shortuser': _('User:'), 'mqoriginalparent': _('Original Parent:') } def __init__(self): pass def get_data(self, item, widget, ctx, custom, **kargs): args = (widget, ctx, custom) def default_func(widget, item, ctx): return None def preset_func(widget, item, ctx): if item == 'rev': revnum = self.get_data('revnum', *args) revid = self.get_data('revid', *args) if revid: return (revnum, revid) return None elif item == 'revnum': return ctx.rev() elif item == 'revid': return str(ctx) elif item == 'desc': return hglib.tounicode(ctx.description().replace('\0', '')) elif item == 'summary': summary = hglib.longsummary( ctx.description().replace('\0', '')) if len(summary) == 0: return None return summary elif item == 'user': user = hglib.user(ctx) if user: return hglib.tounicode(user) return None elif item == 'shortuser': return hglib.tounicode(hglib.username(hglib.user(ctx))) elif item == 'dateage': date = self.get_data('date', *args) age = self.get_data('age', *args) if date and age: return (date, age) return None elif item == 'date': date = ctx.date() if date: return hglib.displaytime(date) return None elif item == 'age': date = ctx.date() if date: return hglib.age(date).decode('utf-8') return None elif item == 'rawbranch': return ctx.branch() or None elif item == 'branch': value = self.get_data('rawbranch', *args) if value: repo = ctx._repo if ctx.node() not in repo.branchtags().values(): return None if value in repo.deadbranches: return None return value return None elif item == 'close': return ctx.extra().get('close') elif item == 'tags': return hglib.getctxtags(ctx) elif item == 'graft': extra = ctx.extra() try: return extra['source'] except KeyError: pass return None elif item == 'transplant': extra = ctx.extra() try: ts = extra['transplant_source'] if ts: return binascii.hexlify(ts) except KeyError: pass return None elif item == 'obsolete': obsoletestate = [] if ctx.obsolete(): obsoletestate.append('obsolete') if ctx.extinct(): obsoletestate.append('extinct') obsoletestate += ctx.troubles() if obsoletestate: return obsoletestate return None elif item == 'p4': extra = ctx.extra() p4cl = extra.get('p4', None) return p4cl and ('changelist %s' % p4cl) elif item == 'svn': extra = ctx.extra() cvt = extra.get('convert_revision', '') if cvt.startswith('svn:'): result = cvt.split('/', 1)[-1] if cvt != result: return result return cvt.split('@')[-1] else: return None elif item == 'converted': extra = ctx.extra() cvt = extra.get('convert_revision', '') if cvt and not cvt.startswith('svn:'): return cvt else: return None elif item == 'ishead': childbranches = [cctx.branch() for cctx in ctx.children()] return ctx.branch() not in childbranches elif item == 'mqoriginalparent': target = ctx.thgmqoriginalparent() if not target: return None p1 = ctx.p1() if p1 is not None and p1.hex() == target: return None if target not in ctx._repo: return None return target raise UnknownItem(item) if 'data' in custom and not kargs.get('usepreset', False): try: return custom['data'](widget, item, ctx) except UnknownItem: pass try: return preset_func(widget, item, ctx) except UnknownItem: pass return default_func(widget, item, ctx) def get_label(self, item, widget, ctx, custom, **kargs): def default_func(widget, item): return '' def preset_func(widget, item): try: return self.LABELS[item] except KeyError: raise UnknownItem(item) if 'label' in custom and not kargs.get('usepreset', False): try: return custom['label'](widget, item, ctx) except UnknownItem: pass try: return preset_func(widget, item) except UnknownItem: pass return default_func(widget, item) def get_markup(self, item, widget, ctx, custom, **kargs): args = (widget, ctx, custom) mono = dict(family='monospace', size='9pt', space='pre') def default_func(widget, item, value): return '' def preset_func(widget, item, value): if item == 'rev': revnum, revid = value revid = qtlib.markup(revid, **mono) if revnum is not None and revid is not None: return '%s (%s)' % (revnum, revid) return '%s' % revid elif item in ('revid', 'graft', 'transplant', 'mqoriginalparent'): return qtlib.markup(value, **mono) elif item in ('revnum', 'p4', 'close', 'converted'): return str(value) elif item == 'svn': # svn is always in utf-8 because ctx.extra() isn't converted return unicode(value, 'utf-8', 'replace') elif item in ('rawbranch', 'branch'): opts = dict(fg='black', bg='#aaffaa') return qtlib.markup(' %s ' % value, **opts) elif item == 'tags': opts = dict(fg='black', bg='#ffffaa') tags = [qtlib.markup(' %s ' % tag, **opts) for tag in value] return ' '.join(tags) elif item in ('desc', 'summary', 'user', 'shortuser', 'date', 'age'): return qtlib.markup(value) elif item == 'dateage': return qtlib.markup('%s (%s)' % value) elif item == 'obsolete': opts = dict(fg='black', bg='#ff8566') obsoletestates = [qtlib.markup(' %s ' % state, **opts) for state in value] return ' '.join(obsoletestates) raise UnknownItem(item) value = self.get_data(item, *args) if value is None: return None if 'markup' in custom and not kargs.get('usepreset', False): try: return custom['markup'](widget, item, value) except UnknownItem: pass try: return preset_func(widget, item, value) except UnknownItem: pass return default_func(widget, item, value) def get_widget(self, item, widget, ctx, custom, **kargs): args = (widget, ctx, custom) def default_func(widget, item, markups): if isinstance(markups, basestring): markups = (markups,) labels = [] for text in markups: label = QLabel() label.setText(text) labels.append(label) return labels markups = self.get_markup(item, *args) if not markups: return None if 'widget' in custom and not kargs.get('usepreset', False): try: return custom['widget'](widget, item, markups) except UnknownItem: pass return default_func(widget, item, markups) class SummaryBase(object): def __init__(self, target, custom, repo, info): if target is None: self.target = None else: self.target = str(target) self.custom = custom self.repo = repo self.info = info self.ctx = repo.changectx(self.target) def get_data(self, item, **kargs): return self.info.get_data(item, self, self.ctx, self.custom, **kargs) def get_label(self, item, **kargs): return self.info.get_label(item, self, self.ctx, self.custom, **kargs) def get_markup(self, item, **kargs): return self.info.get_markup(item, self, self.ctx, self.custom, **kargs) def get_widget(self, item, **kargs): return self.info.get_widget(item, self, self.ctx, self.custom, **kargs) def set_revision(self, rev): self.target = rev def update(self, target=None, custom=None, repo=None): self.ctx = None if target is None: target = self.target if target is not None: target = str(target) self.target = target if custom is not None: self.custom = custom if repo is None: repo = self.repo if repo is not None: self.repo = repo if self.ctx is None: self.ctx = repo.changectx(target) PANEL_TMPL = '%s%s' class SummaryPanel(SummaryBase, QWidget): linkActivated = pyqtSignal(QString) def __init__(self, target, style, custom, repo, info): SummaryBase.__init__(self, target, custom, repo, info) QWidget.__init__(self) self.csstyle = style hbox = QHBoxLayout() hbox.setMargin(0) hbox.setSpacing(0) self.setLayout(hbox) self.revlabel = None self.expand_btn = qtlib.PMButton() def update(self, target=None, style=None, custom=None, repo=None): SummaryBase.update(self, target, custom, repo) if style is not None: self.csstyle = style if self.revlabel is None: self.revlabel = QLabel() self.revlabel.linkActivated.connect(self.linkActivated) self.layout().addWidget(self.revlabel, 0, Qt.AlignTop) if 'expandable' in self.csstyle and self.csstyle['expandable']: if self.expand_btn.parentWidget() is None: self.expand_btn.clicked.connect(lambda: self.update()) margin = QHBoxLayout() margin.setMargin(3) margin.addWidget(self.expand_btn, 0, Qt.AlignTop) self.layout().insertLayout(0, margin) self.expand_btn.setShown(True) elif self.expand_btn.parentWidget() is not None: self.expand_btn.setHidden(True) interact = Qt.LinksAccessibleByMouse if 'selectable' in self.csstyle and self.csstyle['selectable']: interact |= Qt.TextBrowserInteraction self.revlabel.setTextInteractionFlags(interact) # build info contents = self.csstyle.get('contents', ()) if 'expandable' in self.csstyle and self.csstyle['expandable'] \ and self.expand_btn.is_collapsed(): contents = contents[0:1] if 'margin' in self.csstyle: margin = self.csstyle['margin'] assert isinstance(margin, (int, long)) buf = '' % margin else: buf = '
' for item in contents: markups = self.get_markup(item) if not markups: continue label = qtlib.markup(self.get_label(item), weight='bold') if isinstance(markups, basestring): markups = [markups,] buf += PANEL_TMPL % (label, markups.pop(0)) for markup in markups: buf += PANEL_TMPL % (' ', markup) buf += '
' self.revlabel.setText(buf) return True def set_expanded(self, state): self.expand_btn.set_expanded(state) self.update() def is_expanded(self): return self.expand_btn.is_expanded() def minimumSizeHint(self): s = QWidget.minimumSizeHint(self) return QSize(0, s.height()) LABEL_PAT = re.compile(r'(?:(?<=%%)|(? # # rupdate.py - Remote Update dialog for TortoiseHg # # This dialog lets users update a remote ssh repository. # # Requires a copy of the rupdate plugin found at: # http://bitbucket.org/MrWerewolf/rupdate # # Also, enable the plugin with the following in mercurial.ini: # # [extensions] # rupdate = /path/to/rupdate # # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error, node from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import hgrcutil from tortoisehg.hgqt.update import UpdateDialog from PyQt4.QtCore import * from PyQt4.QtGui import * class rUpdateDialog(UpdateDialog): def __init__(self, repoagent, rev=None, parent=None, opts={}): super(rUpdateDialog, self).__init__(repoagent, rev, parent, opts) # Get configured paths self.paths = {} fn = self.repo.join('hgrc') fn, cfg = hgrcutil.loadIniFile([fn], self) if 'paths' in cfg: for alias in cfg['paths']: self.paths[ alias ] = cfg['paths'][alias] ### target path combo self.path_combo = pcombo = QComboBox() pcombo.setEditable(True) for alias in self.paths: pcombo.addItem(hglib.tounicode(self.paths[alias])) ### shift existing items down a row. for i in range(self.grid.count()-1, -1, -1): row, col, rowSp, colSp = self.grid.getItemPosition(i) item = self.grid.takeAt(i) self.grid.removeItem(item) self.grid.addItem(item, row + 1, col, rowSp, colSp, item.alignment()) ### add target path combo to grid self.grid.addWidget(QLabel(_('Location:')), 0, 0) self.grid.addWidget(pcombo, 0, 1) ### Options self.discard_chk.setText(_('Discard remote changes, no backup ' '(-C/--clean)')) self.push_chk = QCheckBox(_('Perform a push before updating' ' (-p/--push)')) self.newbranch_chk = QCheckBox(_('Allow pushing new branches' ' (--new-branch)')) self.force_chk = QCheckBox(_('Force push to remote location' ' (-f/--force)')) self.optbox.removeWidget(self.showlog_chk) self.optbox.addWidget(self.push_chk) self.optbox.addWidget(self.newbranch_chk) self.optbox.addWidget(self.force_chk) self.optbox.addWidget(self.showlog_chk) #### Persisted Options self.push_chk.setChecked( QSettings().value('rupdate/push', False).toBool()) self.newbranch_chk.setChecked( QSettings().value('rupdate/newbranch', False).toBool()) self.showlog_chk.setChecked( QSettings().value('rupdate/showlog', False).toBool()) # prepare to show self.push_chk.setHidden(True) self.newbranch_chk.setHidden(True) self.force_chk.setHidden(True) self.showlog_chk.setHidden(True) self.update_info() # expand options if a hidden one is checked self.show_options(self.hiddenSettingIsChecked()) ### Private Methods ### def hiddenSettingIsChecked(self): # This might be called from the super class before all options are built. # So, we need to check to make sure these options exist first. if (getattr(self, "push_chk", None) and self.push_chk.isChecked() ) or (getattr(self, "newbranch_chk", None) and self.newbranch_chk.isChecked() ) or (getattr(self, "force_chk", None) and self.force_chk.isChecked() ) or (getattr(self, "showlog_chk", None) and self.showlog_chk.isChecked()): return True else: return False def saveSettings(self): QSettings().setValue('rupdate/push', self.push_chk.isChecked()) QSettings().setValue('rupdate/newbranch', self.newbranch_chk.isChecked()) QSettings().setValue('rupdate/showlog', self.showlog_chk.isChecked()) def update_info(self): super(rUpdateDialog, self).update_info() # Keep update button enabled. self.update_btn.setDisabled(False) def update(self): self.saveSettings() cmdline = ['rupdate', '--repository', self.repo.root] if self.discard_chk.isChecked(): cmdline.append('--clean') if self.push_chk.isChecked(): cmdline.append('--push') if self.newbranch_chk.isChecked(): cmdline.append('--new-branch') if self.force_chk.isChecked(): cmdline.append('--force') dest = hglib.fromunicode(self.path_combo.currentText()) cmdline.append('-d') cmdline.append(dest) # Refer to the revision by the short hash. rev = hglib.fromunicode(self.rev_combo.currentText()) revHash = self.repo[rev].hex() cmdline.append(revHash) # start updating self.repo.incrementBusyCount() self.cmd.run(cmdline) ### Signal Handlers ### def show_options(self, visible): # Like hiddenSettingIsChecked(), need to make sure these options exist first. if getattr(self, "push_chk", None): self.push_chk.setShown(visible) if getattr(self, "newbranch_chk", None): self.newbranch_chk.setShown(visible) if getattr(self, "force_chk", None): self.force_chk.setShown(visible) if getattr(self, "showlog_chk", None): self.showlog_chk.setShown(visible) def command_started(self): super(rUpdateDialog, self).command_started() self.update_btn.setHidden(False) tortoisehg-2.10/tortoisehg/hgqt/reporegistry.py0000644000076400007640000010276712235634453021157 0ustar stevesteve# reporegistry.py - registry for a user's repositories # # Copyright 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os from mercurial import commands, hg, ui, util from tortoisehg.util import hglib, paths from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, repotreemodel, clone, settings from PyQt4.QtCore import * from PyQt4.QtGui import * def settingsfilename(): """Return path to thg-reporegistry.xml as unicode""" s = QSettings() dir = os.path.dirname(unicode(s.fileName())) return dir + '/' + 'thg-reporegistry.xml' class RepoTreeView(QTreeView): showMessage = pyqtSignal(QString) menuRequested = pyqtSignal(object, object) openRepo = pyqtSignal(QString, bool) dropAccepted = pyqtSignal() updateSettingsFile = pyqtSignal() def __init__(self, parent): QTreeView.__init__(self, parent, allColumnsShowFocus=True) self.selitem = None self.msg = '' self.setHeaderHidden(True) self.setExpandsOnDoubleClick(False) self.setMouseTracking(True) # enable drag and drop # (see http://doc.qt.nokia.com/4.6/model-view-dnd.html) self.setDragEnabled(True) self.setAcceptDrops(True) self.setAutoScroll(True) self.setDragDropMode(QAbstractItemView.DragDrop) if PYQT_VERSION >= 0x40700: self.setDefaultDropAction(Qt.MoveAction) self.setDropIndicatorShown(True) self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed) self.setSelectionBehavior(QAbstractItemView.SelectRows) QShortcut('Return', self, self.showFirstTabOrOpen).setContext( Qt.WidgetShortcut) QShortcut('Enter', self, self.showFirstTabOrOpen).setContext( Qt.WidgetShortcut) QShortcut('Delete', self, self.removeSelected).setContext( Qt.WidgetShortcut) def contextMenuEvent(self, event): if not self.selitem: return self.menuRequested.emit(event.globalPos(), self.selitem) def dragEnterEvent(self, event): if event.source() is self: # Use the default event handler for internal dragging super(RepoTreeView, self).dragEnterEvent(event) return d = event.mimeData() for u in d.urls(): root = paths.find_root(hglib.fromunicode(u.toLocalFile())) if root: event.setDropAction(Qt.LinkAction) event.accept() self.setState(QAbstractItemView.DraggingState) break def dropLocation(self, event): index = self.indexAt(event.pos()) # Determine where the item was dropped. target = index.internalPointer() if not target.isRepo(): group = index row = -1 else: indicator = self.dropIndicatorPosition() group = index.parent() row = index.row() if indicator == QAbstractItemView.BelowItem: row = index.row() + 1 return index, group, row def startDrag(self, supportedActions): indexes = self.selectedIndexes() # Make sure that all selected items are of the same type if len(indexes) == 0: # Nothing to drag! return # Make sure that all items that we are dragging are of the same type firstItem = indexes[0].internalPointer() selectionInstanceType = type(firstItem) for idx in indexes[1:]: if selectionInstanceType != type(idx.internalPointer()): # Cannot drag mixed type items return # Each item type may support different drag & drop actions # For instance, suprepo items support Copy actions only supportedActions = firstItem.getSupportedDragDropActions() super(RepoTreeView, self).startDrag(supportedActions) def dropEvent(self, event): data = event.mimeData() index, group, row = self.dropLocation(event) if index: m = self.model() if event.source() is self: # Event is an internal move, so pass it to the model col = 0 if m.dropMimeData(data, event.dropAction(), row, col, group): event.accept() self.dropAccepted.emit() else: # Event is a drop of an external repo accept = False for u in data.urls(): uroot = paths.find_root(unicode(u.toLocalFile())) if uroot and not m.isKnownRepoRoot(uroot, standalone=True): repoindex = m.addRepo(uroot, row, group) m.loadSubrepos(repoindex) accept = True if accept: event.setDropAction(Qt.LinkAction) event.accept() self.dropAccepted.emit() self.setAutoScroll(False) self.setState(QAbstractItemView.NoState) self.viewport().update() self.setAutoScroll(True) def mouseMoveEvent(self, event): self.msg = '' pos = event.pos() idx = self.indexAt(pos) if idx.isValid(): item = idx.internalPointer() self.msg = item.details() self.showMessage.emit(self.msg) if event.buttons() == Qt.NoButton: # Bail out early to avoid tripping over this bug: # http://bugreports.qt.nokia.com/browse/QTBUG-10180 return super(RepoTreeView, self).mouseMoveEvent(event) def leaveEvent(self, event): if self.msg != '': self.showMessage.emit('') def mouseDoubleClickEvent(self, event): if self.selitem and self.selitem.internalPointer().isRepo(): # We can only open mercurial repositories and subrepositories repotype = self.selitem.internalPointer().repotype() if repotype == 'hg': self.showFirstTabOrOpen() else: qtlib.WarningMsgBox( _('Unsupported repository type (%s)') % repotype, _('Cannot open non Mercurial repositories or subrepositories'), parent=self) else: # a double-click on non-repo rows opens an editor super(RepoTreeView, self).mouseDoubleClickEvent(event) def selectionChanged(self, selected, deselected): selection = self.selectedIndexes() if len(selection) == 0: self.selitem = None else: self.selitem = selection[0] def sizeHint(self): size = super(RepoTreeView, self).sizeHint() size.setWidth(QFontMetrics(self.font()).width('M') * 15) return size def showFirstTabOrOpen(self): 'Enter or double click events, show existing or open a new repowidget' if self.selitem and self.selitem.internalPointer().isRepo(): root = self.selitem.internalPointer().rootpath() self.openRepo.emit(hglib.tounicode(root), True) def removeSelected(self): 'remove selected repository' s = self.selitem if not s: return item = s.internalPointer() if 'remove' not in item.menulist(): # check capability return if not item.okToDelete(): labels = [(QMessageBox.Yes, _('&Delete')), (QMessageBox.No, _('Cancel'))] if not qtlib.QuestionMsgBox(_('Confirm Delete'), _("Delete Group '%s' and all its entries?")% item.name, labels=labels, parent=self): return m = self.model() row = s.row() parent = s.parent() m.removeRows(row, 1, parent) self.selectionChanged(None, None) self.updateSettingsFile.emit() class RepoRegistryView(QDockWidget): showMessage = pyqtSignal(QString) openRepo = pyqtSignal(QString, bool) removeRepo = pyqtSignal(QString) progressReceived = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repomanager, parent): QDockWidget.__init__(self, parent) self._repomanager = repomanager self.watcher = None self._setupSettingActions() self.setFeatures(QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.setWindowTitle(_('Repository Registry')) mainframe = QFrame() mainframe.setLayout(QVBoxLayout()) self.setWidget(mainframe) mainframe.layout().setContentsMargins(0, 0, 0, 0) self.contextmenu = QMenu(self) self.tview = tv = RepoTreeView(self) mainframe.layout().addWidget(tv) tv.setIndentation(10) tv.setFirstColumnSpanned(0, QModelIndex(), True) tv.setColumnHidden(1, True) tv.showMessage.connect(self.showMessage) tv.menuRequested.connect(self.onMenuRequest) tv.openRepo.connect(self.openRepo) tv.updateSettingsFile.connect(self.updateSettingsFile) tv.dropAccepted.connect(self.dropAccepted) self.createActions() self._loadSettings() self._updateSettingActions() sfile = settingsfilename() model = repotreemodel.RepoTreeModel(sfile, repomanager, self, showShortPaths=self._isSettingEnabled('showShortPaths')) tv.setModel(model) # Setup a file system watcher to update the reporegistry # anytime it is modified by another thg instance # Note that we must make sure that the settings file exists before # setting thefile watcher if not os.path.exists(sfile): if not os.path.exists(os.path.dirname(sfile)): os.makedirs(os.path.dirname(sfile)) tv.model().write(sfile) self.watcher = QFileSystemWatcher(self) self.watcher.addPath(sfile) self._reloadModelTimer = QTimer(self, interval=2000, singleShot=True) self._reloadModelTimer.timeout.connect(self.reloadModel) self.watcher.fileChanged.connect(self._reloadModelTimer.start) QTimer.singleShot(0, self._initView) @pyqtSlot() def _initView(self): self.expand() self._updateColumnVisibility() if self._isSettingEnabled('showSubrepos'): self._scanAllRepos() def _loadSettings(self): defaultmap = {'showPaths': False, 'showSubrepos': False, 'showNetworkSubrepos': False, 'showShortPaths': True} s = QSettings() s.beginGroup('Workbench') # for compatibility with old release for key, action in self._settingactions.iteritems(): action.setChecked(s.value(key, defaultmap[key]).toBool()) s.endGroup() def _saveSettings(self): s = QSettings() s.beginGroup('Workbench') # for compatibility with old release for key, action in self._settingactions.iteritems(): s.setValue(key, action.isChecked()) s.endGroup() def _setupSettingActions(self): settingtable = [ ('showPaths', _('Show &Paths'), self._updateColumnVisibility), ('showShortPaths', _('Show S&hort Paths'), self._updateCommonPath), ('showSubrepos', _('&Scan Repositories at Startup'), None), ('showNetworkSubrepos', _('Scan &Remote Repositories'), None), ] self._settingactions = {} for i, (key, text, slot) in enumerate(settingtable): a = QAction(text, self, checkable=True) a.setData(i) # sort key if slot: a.triggered.connect(slot) a.triggered.connect(self._updateSettingActions) self._settingactions[key] = a @pyqtSlot() def _updateSettingActions(self): ax = self._settingactions ax['showNetworkSubrepos'].setEnabled(ax['showSubrepos'].isChecked()) ax['showShortPaths'].setEnabled(ax['showPaths'].isChecked()) def settingActions(self): return sorted(self._settingactions.itervalues(), key=lambda a: a.data().toInt()) def _isSettingEnabled(self, key): return self._settingactions[key].isChecked() @pyqtSlot() def _updateCommonPath(self): show = self._isSettingEnabled('showShortPaths') self.tview.model().updateCommonPaths(show) # FIXME: access violation; should be done by model self.tview.dataChanged(QModelIndex(), QModelIndex()) def updateSettingsFile(self): # If there is a settings watcher, we must briefly stop watching the # settings file while we save it, otherwise we'll get the update signal # that we do not want sfile = settingsfilename() if self.watcher: self.watcher.removePath(sfile) self.tview.model().write(sfile) if self.watcher: self.watcher.addPath(sfile) # Whenver the settings file must be updated, it is also time to ensure # that the commonPaths are up to date QTimer.singleShot(0, self.tview.model().updateCommonPaths) @pyqtSlot() def dropAccepted(self): # Whenever a drag and drop operation is completed, update the settings # file QTimer.singleShot(0, self.updateSettingsFile) @pyqtSlot() def reloadModel(self): oldmodel = self.tview.model() activeroot = oldmodel.repoRoot(oldmodel.activeRepoIndex()) newmodel = repotreemodel.RepoTreeModel(settingsfilename(), self._repomanager, self, self._isSettingEnabled('showShortPaths')) self.tview.setModel(newmodel) oldmodel.deleteLater() if self._isSettingEnabled('showSubrepos'): self._scanAllRepos() self.expand() if activeroot: self.setActiveTabRepo(activeroot) self._reloadModelTimer.stop() def expand(self): self.tview.expandToDepth(0) def addRepo(self, uroot): """Add repo if not exists; called when the workbench has opened it""" m = self.tview.model() knownindex = m.indexFromRepoRoot(uroot) if knownindex.isValid(): self._scanAddedRepo(knownindex) # just scan stale subrepos else: index = m.addRepo(uroot) self._scanAddedRepo(index) self.updateSettingsFile() def setActiveTabRepo(self, root): """"The selected tab has changed on the workbench""" m = self.tview.model() index = m.indexFromRepoRoot(root) m.setActiveRepo(index) self.tview.scrollTo(index) @pyqtSlot() def _updateColumnVisibility(self): show = self._isSettingEnabled('showPaths') self.tview.setColumnHidden(1, not show) self.tview.setHeaderHidden(not show) if show: self.tview.resizeColumnToContents(0) self.tview.resizeColumnToContents(1) def close(self): # We must stop monitoring the settings file and then we can save it sfile = settingsfilename() self.watcher.removePath(sfile) self.tview.model().write(sfile) self._saveSettings() def _action_defs(self): a = [("reloadRegistry", _("&Refresh Repository List"), 'view-refresh', _("Refresh the Repository Registry list"), self.reloadModel), ("open", _("&Open"), 'thg-repository-open', _("Open the repository in a new tab"), self.open), ("openAll", _("&Open All"), 'thg-repository-open', _("Open all repositories in new tabs"), self.openAll), ("newGroup", _("New &Group"), 'new-group', _("Create a new group"), self.newGroup), ("rename", _("Re&name"), None, _("Rename the entry"), self.startRename), ("settings", _("Settin&gs"), 'settings_user', _("View the repository's settings"), self.startSettings), ("remove", _("Re&move from Registry"), 'menudelete', _("Remove the node and all its subnodes." " Repositories are not deleted from disk."), self.removeSelected), ("clone", _("Clon&e..."), 'hg-clone', _("Clone Repository"), self.cloneRepo), ("explore", _("E&xplore"), 'system-file-manager', _("Open the repository in a file browser"), self.explore), ("terminal", _("&Terminal"), 'utilities-terminal', _("Open a shell terminal in the repository root"), self.terminal), ("add", _("&Add Repository..."), 'hg', _("Add a repository to this group"), self.addNewRepo), ("addsubrepo", _("A&dd Subrepository..."), 'thg-add-subrepo', _("Convert an existing repository into a subrepository"), self.addSubrepo), ("removesubrepo", _("Remo&ve Subrepository..."), 'thg-remove-subrepo', _("Remove this subrepository from the current revision"), self.removeSubrepo), ("copypath", _("Copy &Path"), '', _("Copy the root path of the repository to the clipboard"), self.copyPath), ("sortbyname", _("Sort by &Name"), '', _("Sort the group by short name"), self.sortbyname), ("sortbypath", _("Sort by &Path"), '', _("Sort the group by full path"), self.sortbypath), ("sortbyhgsub", _("&Sort by .hgsub"), '', _("Order the subrepos as in .hgsub"), self.sortbyhgsub), ] return a def createActions(self): self._actions = {} for name, desc, icon, tip, cb in self._action_defs(): self._actions[name] = QAction(desc, self) QTimer.singleShot(0, self.configureActions) def configureActions(self): for name, desc, icon, tip, cb in self._action_defs(): act = self._actions[name] if icon: act.setIcon(qtlib.geticon(icon)) if tip: act.setStatusTip(tip) if cb: act.triggered.connect(cb) self.addAction(act) def onMenuRequest(self, point, selitem): menulist = selitem.internalPointer().menulist() if not menulist: return self.addtomenu(self.contextmenu, menulist) self.selitem = selitem self.contextmenu.exec_(point) def addtomenu(self, menu, actlist): menu.clear() for act in actlist: if isinstance(act, basestring) and act in self._actions: menu.addAction(self._actions[act]) elif isinstance(act, tuple) and len(act) == 2: submenu = menu.addMenu(act[0]) self.addtomenu(submenu, act[1]) else: menu.addSeparator() # ## Menu action handlers # def cloneRepo(self): root = self.selitem.internalPointer().rootpath() d = clone.CloneDialog(args=[root, root + '-clone'], parent=self) d.finished.connect(d.deleteLater) d.clonedRepository.connect(self._openClone) d.show() def explore(self): root = self.selitem.internalPointer().rootpath() qtlib.openlocalurl(root) def terminal(self): repoitem = self.selitem.internalPointer() qtlib.openshell(repoitem.rootpath(), repoitem.shortname()) def addNewRepo(self): 'menu action handler for adding a new repository' caption = _('Select repository directory to add') FD = QFileDialog path = FD.getExistingDirectory(caption=caption, options=FD.ShowDirsOnly | FD.ReadOnly) if path: m = self.tview.model() uroot = paths.find_root(unicode(path)) if uroot and not m.isKnownRepoRoot(uroot, standalone=True): index = m.addRepo(uroot, parent=self.selitem) self._scanAddedRepo(index) def addSubrepo(self): 'menu action handler for adding a new subrepository' root = hglib.tounicode(self.selitem.internalPointer().rootpath()) caption = _('Select an existing repository to add as a subrepo') FD = QFileDialog path = unicode(FD.getExistingDirectory(caption=caption, directory=root, options=FD.ShowDirsOnly | FD.ReadOnly)) if path: path = os.path.normpath(path) sroot = paths.find_root(path) root = os.path.normcase(os.path.normpath(root)) if not sroot: qtlib.WarningMsgBox(_('Cannot add subrepository'), _('%s is not a valid repository') % path, parent=self) return elif not os.path.isdir(sroot): qtlib.WarningMsgBox(_('Cannot add subrepository'), _('"%s" is not a folder') % sroot, parent=self) return elif os.path.normcase(sroot) == root: qtlib.WarningMsgBox(_('Cannot add subrepository'), _('A repository cannot be added as a subrepo of itself'), parent=self) return elif root != paths.find_root(os.path.dirname(os.path.normcase(path))): qtlib.WarningMsgBox(_('Cannot add subrepository'), _('The selected folder:

%s

' 'is not inside the target repository.

' 'This may be allowed but is greatly discouraged.
' 'If you want to add a non trivial subrepository mapping ' 'you must manually edit the .hgsub file') % root, parent=self) return else: # The selected path is the root of a repository that is inside # the selected repository # Use forward slashes for relative subrepo root paths srelroot = sroot[len(root)+1:] srelroot = util.pconvert(srelroot) # Is is already on the selected repository substate list? try: repo = hg.repository(ui.ui(), hglib.fromunicode(root)) except: qtlib.WarningMsgBox(_('Cannot open repository'), _('The selected repository:

%s

' 'cannot be open!') % root, parent=self) return if hglib.fromunicode(srelroot) in repo['.'].substate: qtlib.WarningMsgBox(_('Subrepository already exists'), _('The selected repository:

%s

' 'is already a subrepository of:

%s

' 'as: "%s"') % (sroot, root, srelroot), parent=self) return else: # Read the current .hgsub file contents lines = [] hasHgsub = os.path.exists(repo.wjoin('.hgsub')) if hasHgsub: try: fsub = repo.wopener('.hgsub', 'r') lines = fsub.readlines() fsub.close() except: qtlib.WarningMsgBox( _('Failed to add subrepository'), _('Cannot open the .hgsub file in:

%s') \ % root, parent=self) return # Make sure that the selected subrepo (or one of its # subrepos!) is not already on the .hgsub file linesep = '' # On Windows case is unimportant, while on posix it is srelrootnormcase = os.path.normcase(srelroot) for line in lines: line = hglib.tounicode(line) spath = line.split("=")[0].strip() if not spath: continue if not linesep: linesep = hglib.getLineSeparator(line) spath = util.pconvert(spath) if os.path.normcase(spath) == srelrootnormcase: qtlib.WarningMsgBox( _('Failed to add repository'), _('The .hgsub file already contains the ' 'line:

%s') % line, parent=self) return if not linesep: linesep = os.linesep # Append the new subrepo to the end of the .hgsub file lines.append(hglib.fromunicode('%s = %s' % (srelroot, srelroot))) lines = [line.strip(linesep) for line in lines] # and update the .hgsub file try: fsub = repo.wopener('.hgsub', 'w') fsub.write(linesep.join(lines) + linesep) fsub.close() if not hasHgsub: commands.add(ui.ui(), repo, repo.wjoin('.hgsub')) qtlib.InfoMsgBox( _('Subrepo added to .hgsub file'), _('The selected subrepo:

%s

' 'has been added to the .hgsub file of the repository:

%s

' 'Remember that in order to finish adding the ' 'subrepo you must still commit the ' 'changes to the .hgsub file in order to confirm ' 'the addition of the subrepo.') \ % (srelroot, root), parent=self) except: qtlib.WarningMsgBox( _('Failed to add repository'), _('Cannot update the .hgsub file in:

%s') \ % root, parent=self) return def removeSubrepo(self): 'menu action handler for removing an existing subrepository' path = hglib.tounicode(self.selitem.internalPointer().rootpath()) containerpath = os.path.normpath(os.path.join(path, '..')) root = paths.find_root(containerpath) relsubpath = os.path.normcase(os.path.normpath(path[1+len(root):])) hgsubfilename = os.path.join(root, '.hgsub') try: f = open(hgsubfilename, 'r') hgsub = [] found = False for line in f.readlines(): spath = os.path.normcase( os.path.normpath( line.split('=')[0].strip())) if spath != relsubpath: hgsub.append(line) else: found = True f.close() except IOError: qtlib.ErrorMsgBox(_('Could not open .hgsub file'), _('Cannot read the .hgsub file.

' 'Subrepository removal failed.'), parent=self) return if not found: qtlib.WarningMsgBox(_('Subrepository not found'), _('The selected subrepository was not found ' 'on the .hgsub file.

' 'Perhaps it has already been removed?'), parent=self) return choices = (_('&Yes'), _('&No')) answer = qtlib.CustomPrompt(_('Remove the selected repository?'), _('Do you really want to remove the repository "%s" ' 'from its parent repository "%s"') % (relsubpath, root), self, choices=choices, default=choices[0]).run() if answer != 0: return try: f = open(hgsubfilename, 'w') f.writelines(hgsub) f.close() qtlib.InfoMsgBox(_('Subrepository removed from .hgsub'), _('The selected subrepository has been removed ' 'from the .hgsub file.

' 'Remember that you must commit this .hgsub change in order ' 'to complete the removal of the subrepository!'), parent=self) except IOError: qtlib.ErrorMsgBox(_('Could not update .hgsub file'), _('Cannot update the .hgsub file.

' 'Subrepository removal failed.'), parent=self) def startSettings(self): root = self.selitem.internalPointer().rootpath() sd = settings.SettingsDialog(configrepo=True, focus='web.name', parent=self, root=root) sd.finished.connect(sd.deleteLater) sd.exec_() def openAll(self): for root in self.selitem.internalPointer().childRoots(): self.openRepo.emit(hglib.tounicode(root), False) @pyqtSlot(unicode, unicode) def _openClone(self, root, sourceroot): m = self.tview.model() src = m.indexFromRepoRoot(sourceroot, standalone=True) if src.isValid() and not m.isKnownRepoRoot(root): index = m.addRepo(root, parent=src.parent()) self._scanAddedRepo(index) self.open(root) def open(self, root=None): 'open context menu action, open repowidget unconditionally' if not root: root = self.selitem.internalPointer().rootpath() repotype = self.selitem.internalPointer().repotype() else: root = hglib.fromunicode(root) if os.path.exists(os.path.join(root, '.hg')): repotype = 'hg' else: repotype = 'unknown' if repotype == 'hg': self.openRepo.emit(hglib.tounicode(root), False) else: qtlib.WarningMsgBox( _('Unsupported repository type (%s)') % repotype, _('Cannot open non Mercurial repositories or subrepositories'), parent=self) def copyPath(self): clip = QApplication.clipboard() clip.setText(hglib.tounicode(self.selitem.internalPointer().rootpath())) def startRename(self): self.tview.edit(self.tview.currentIndex()) def newGroup(self): self.tview.model().addGroup(_('New Group')) def removeSelected(self): ip = self.selitem.internalPointer() if ip.isRepo(): root = ip.rootpath() else: root = None self.tview.removeSelected() if root is not None: self.removeRepo.emit(hglib.tounicode(root)) def sortbyname(self): childs = self.selitem.internalPointer().childs self.tview.model().sortchilds(childs, lambda x: x.shortname().lower()) def sortbypath(self): childs = self.selitem.internalPointer().childs self.tview.model().sortchilds( childs, lambda x: os.path.normcase(util.normpath(x.rootpath()))) def sortbyhgsub(self): ip = self.selitem.internalPointer() repo = hg.repository(ui.ui(), ip.rootpath()) ctx = repo['.'] wfile = '.hgsub' if wfile not in ctx: return self.sortbypath() data = ctx[wfile].data().strip() data = data.split('\n') getsubpath = lambda x: x.split('=')[0].strip() abspath = lambda x: util.normpath(repo.wjoin(x)) hgsuborder = [abspath(getsubpath(x)) for x in data] def keyfunc(x): try: return hgsuborder.index(util.normpath(x.rootpath())) except: # If an item is not found, place it at the top return 0 self.tview.model().sortchilds(ip.childs, keyfunc) def _scanAddedRepo(self, index): m = self.tview.model() invalidpaths = m.loadSubrepos(index) if not invalidpaths: return root = m.repoRoot(index) if root in invalidpaths: qtlib.WarningMsgBox(_('Could not get subrepository list'), _('It was not possible to get the subrepository list for ' 'the repository in:

%s') % root, parent=self) else: qtlib.WarningMsgBox(_('Could not open some subrepositories'), _('It was not possible to fully load the subrepository ' 'list for the repository in:

%s

' 'The following subrepositories may be missing, broken or ' 'on an inconsistent state and cannot be accessed:' '

%s') % (root, "
".join(invalidpaths)), parent=self) @pyqtSlot(QString) def scanRepo(self, uroot): m = self.tview.model() index = m.indexFromRepoRoot(uroot) if index.isValid(): m.loadSubrepos(index) def _scanAllRepos(self): m = self.tview.model() indexes = m.indexesOfRepoItems(standalone=True) if not self._isSettingEnabled('showNetworkSubrepos'): indexes = [idx for idx in indexes if not paths.netdrive_status(m.repoRoot(idx))] topic = _('Updating repository registry') for n, idx in enumerate(indexes): self.progressReceived.emit( topic, n, _('Loading repository %s') % m.repoRoot(idx), '', len(indexes)) m.loadSubrepos(idx) self.progressReceived.emit( topic, None, _('Repository Registry updated'), '', None) tortoisehg-2.10/tortoisehg/hgqt/i18n.py0000644000076400007640000000147012110205646017153 0ustar stevesteve# i18n.py - internationalization support for TortoiseHg # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from tortoisehg.util.i18n import _ as _gettext from tortoisehg.util.i18n import ngettext as _ngettext from tortoisehg.util.i18n import agettext def _(message, context=''): return unicode(_gettext(message, context), 'utf-8') def ngettext(singular, plural, n): return unicode(_ngettext(singular, plural, n), 'utf-8') class localgettext(object): def _(self, message, context=''): return agettext(message, context='') class keepgettext(object): def _(self, message, context=''): return {'id': message, 'str': _(message, context)} tortoisehg-2.10/tortoisehg/hgqt/archive.py0000644000076400007640000003702112231647662020032 0ustar stevesteve# archive.py - TortoiseHg's dialog for archiving a repo revision # # Copyright 2009 Emmanuel Rosa # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error from tortoisehg.hgqt.i18n import _ from tortoisehg.util import hglib from tortoisehg.hgqt import cmdui, qtlib WD_PARENT = _('= Working Directory Parent =') class ArchiveDialog(QDialog): """ Dialog to archive a particular Mercurial revision """ output = pyqtSignal(QString, QString) makeLogVisible = pyqtSignal(bool) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repoagent, rev=None, parent=None): super(ArchiveDialog, self).__init__(parent) self._repoagent = repoagent # main layout self.vbox = QVBoxLayout() self.vbox.setSpacing(6) self.grid = QGridLayout() self.grid.setSpacing(6) self.vbox.addLayout(self.grid) # content selection self.rev_lbl = QLabel(_('Revision:')) self.rev_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.rev_combo = QComboBox() self.rev_combo.setEditable(True) self.rev_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.files_in_rev_chk = QCheckBox( _('Only files modified/created in this revision')) self.subrepos_chk = QCheckBox(_('Recurse into subrepositories')) self.grid.addWidget(self.rev_lbl, 0, 0) self.grid.addWidget(self.rev_combo, 0, 1) self.grid.addWidget(self.files_in_rev_chk, 1, 1) self.grid.addWidget(self.subrepos_chk, 2, 1) # selecting a destination self.dest_lbl = QLabel(_('Destination path:')) self.dest_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.dest_edit = QLineEdit() self.dest_edit.setMinimumWidth(300) self.dest_btn = QPushButton(_('Browse...')) self.dest_btn.setAutoDefault(False) self.grid.addWidget(self.dest_lbl, 3, 0) self.grid.addWidget(self.dest_edit, 3, 1) self.grid.addWidget(self.dest_btn, 3, 2) # archive type selection self.types_lbl = QLabel(_('Archive types:')) self.types_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) def radio(label): return QRadioButton(label, None) self.filesradio = radio(_('Directory of files')) self.tarradio = radio(_('Uncompressed tar archive')) self.tbz2radio = radio(_('Tar archive compressed using bzip2')) self.tgzradio = radio(_('Tar archive compressed using gzip')) self.uzipradio = radio(_('Uncompressed zip archive')) self.zipradio = radio(_('Zip archive compressed using deflate')) self.grid.addWidget(self.types_lbl, 4, 0) self.grid.addWidget(self.filesradio, 4, 1) self.grid.addWidget(self.tarradio, 5, 1) self.grid.addWidget(self.tbz2radio, 6, 1) self.grid.addWidget(self.tgzradio, 7, 1) self.grid.addWidget(self.uzipradio, 8, 1) self.grid.addWidget(self.zipradio, 9, 1) # some extras self.hgcmd_lbl = QLabel(_('Hg command:')) self.hgcmd_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) self.keep_open_chk = QCheckBox(_('Always show output')) self.grid.addWidget(self.hgcmd_lbl, 10, 0) self.grid.addWidget(self.hgcmd_txt, 10, 1) self.grid.addWidget(self.keep_open_chk, 11, 1) # command widget self.cmd = cmdui.Widget(True, True, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandCanceling.connect(self.command_canceling) self.cmd.output.connect(self.output) self.cmd.makeLogVisible.connect(self.makeLogVisible) self.cmd.progress.connect(self.progress) self.cmd.setHidden(True) self.vbox.addWidget(self.cmd) # bottom buttons self.hbox = QHBoxLayout() self.arch_btn = QPushButton(_('&Archive')) self.arch_btn.setDefault(True) self.close_btn = QPushButton(_('&Close')) self.close_btn.setAutoDefault(False) self.close_btn.setFocus() self.detail_btn = QPushButton(_('&Detail')) self.detail_btn.setAutoDefault(False) self.detail_btn.setHidden(True) self.cancel_btn = QPushButton(_('Cancel')) self.cancel_btn.setAutoDefault(False) self.cancel_btn.setHidden(True) self.hbox.addWidget(self.detail_btn) self.hbox.addStretch(0) self.hbox.addWidget(self.arch_btn) self.hbox.addWidget(self.close_btn) self.hbox.addWidget(self.cancel_btn) self.vbox.addLayout(self.hbox) # set default values self.prevtarget = None self.rev_combo.addItem(WD_PARENT) for b in self.repo.branchtags(): self.rev_combo.addItem(hglib.tounicode(b)) tags = list(self.repo.tags()) tags.sort(reverse=True) for t in tags: self.rev_combo.addItem(hglib.tounicode(t)) if rev: text = hglib.tounicode(str(rev)) selectindex = self.rev_combo.findText(text) if selectindex >= 0: self.rev_combo.setCurrentIndex(selectindex) else: self.rev_combo.insertItem(0, text) self.rev_combo.setCurrentIndex(0) self.rev_combo.setMaxVisibleItems(self.rev_combo.count()) self.subrepos_chk.setChecked(self.get_subrepos_present()) self.dest_edit.setText(hglib.tounicode(self.repo.root)) self.filesradio.setChecked(True) self.update_path() # connecting slots self.dest_edit.textEdited.connect(self.dest_edited) self.rev_combo.editTextChanged.connect(self.rev_combo_changed) self.dest_btn.clicked.connect(self.browse_clicked) self.files_in_rev_chk.stateChanged.connect(self.dest_edited) self.subrepos_chk.toggled.connect(self.onSubreposToggled) self.filesradio.toggled.connect(self.update_path) self.tarradio.toggled.connect(self.update_path) self.tbz2radio.toggled.connect(self.update_path) self.tgzradio.toggled.connect(self.update_path) self.uzipradio.toggled.connect(self.update_path) self.zipradio.toggled.connect(self.update_path) self.arch_btn.clicked.connect(self.archive) self.detail_btn.clicked.connect(self.detail_clicked) self.close_btn.clicked.connect(self.close) self.cancel_btn.clicked.connect(self.cancel_clicked) # dialog setting self.setWindowTitle(_('Archive - %s') % self.repo.displayname) self.setWindowIcon(qtlib.geticon('hg-archive')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setLayout(self.vbox) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.rev_combo.setFocus() self._readsettings() @property def repo(self): return self._repoagent.rawRepo() def rev_combo_changed(self): self.subrepos_chk.setChecked(self.get_subrepos_present()) self.update_path() def dest_edited(self): path = hglib.fromunicode(self.dest_edit.text()) type = self.get_selected_archive_type()['type'] self.compose_command(path, type) def browse_clicked(self): """Select the destination directory or file""" dest = unicode(self.dest_edit.text()) if not os.path.exists(dest): dest = os.path.dirname(dest) select = self.get_selected_archive_type() FD = QFileDialog if select['type'] == 'files': caption = _('Select Destination Folder') filter = '' else: caption = _('Select Destination File') ext = '*' + select['ext'] filter = ';;'.join(['%s (%s)' % (select['label'], ext), _('All files (*)')]) response = FD.getSaveFileName(self, caption, dest, filter, None, FD.ReadOnly) if response: self.dest_edit.setText(response) self.update_path() def onSubreposToggled(self): path = hglib.fromunicode(self.dest_edit.text()) type = self.get_selected_archive_type()['type'] self.compose_command(path, type) def get_subrepos_present(self): rev = self.get_selected_rev() try: ctx = self.repo[rev] except (error.LookupError, error.RepoLookupError): return False return '.hgsubstate' in ctx def get_selected_rev(self): rev = self.rev_combo.currentText() if rev == WD_PARENT: rev = '.' else: rev = hglib.fromunicode(rev) return rev def get_selected_archive_type(self): """Return a dictionary describing the selected archive type""" if self.tarradio.isChecked(): return {'type': 'tar', 'ext': '.tar', 'label': _('Tar archives')} elif self.tbz2radio.isChecked(): return {'type': 'tbz2', 'ext': '.tar.bz2', 'label': _('Bzip2 tar archives')} elif self.tgzradio.isChecked(): return {'type': 'tgz', 'ext': '.tar.gz', 'label': _('Gzip tar archives')} elif self.uzipradio.isChecked(): return {'type': 'uzip', 'ext': '.zip', 'label': _('Zip archives')} elif self.zipradio.isChecked(): return {'type': 'zip', 'ext': '.zip', 'label': _('Zip archives')} return {'type': 'files', 'ext': '', 'label': _('Directory of files')} def update_path(self): def remove_ext(path): for ext in ('.tar', '.tar.bz2', '.tar.gz', '.zip'): if path.endswith(ext): return path.replace(ext, '') return path def remove_rev(path): l = '' for i in xrange(self.rev_combo.count() - 1): l += unicode(self.rev_combo.itemText(i)) revs = [rev[0] for rev in l] revs.append(wdrev) if not self.prevtarget is None: revs.append(self.prevtarget) for rev in ['_' + rev for rev in revs]: if path.endswith(rev): return path.replace(rev, '') return path def add_rev(path, rev): return '%s_%s' % (path, rev) def add_ext(path): select = self.get_selected_archive_type() if select['type'] != 'files': path += select['ext'] return path text = unicode(self.rev_combo.currentText()) if len(text) == 0: return wdrev = str(self.repo['.'].rev()) if text == WD_PARENT: text = wdrev else: try: self.repo[hglib.fromunicode(text)] except (error.RepoError, error.LookupError): return path = unicode(self.dest_edit.text()) path = remove_ext(path) path = remove_rev(path) path = add_rev(path, text) path = add_ext(path) self.dest_edit.setText(path) self.prevtarget = text type = self.get_selected_archive_type()['type'] self.compose_command(hglib.fromunicode(path), type) def compose_command(self, dest, type): cmdline = ['archive', '--repository', self.repo.root] rev = self.get_selected_rev() cmdline.append('-r') cmdline.append(rev) if self.subrepos_chk.isChecked(): cmdline.append('-S') cmdline.append('-t') cmdline.append(type) if self.files_in_rev_chk.isChecked(): ctx = self.repo[rev] for f in ctx.files(): cmdline.append('-I') cmdline.append(f) cmdline.append('--') cmdline.append(dest) # dest: local str self.hgcmd_txt.setText(hglib.tounicode('hg ' + ' '.join(cmdline))) return cmdline def archive(self): # verify input type = self.get_selected_archive_type()['type'] dest = unicode(self.dest_edit.text()) if os.path.exists(dest): if type == 'files': if os.path.isfile(dest): qtlib.WarningMsgBox(_('Duplicate Name'), _('The destination "%s" already exists as ' 'a file!') % dest) return False elif os.listdir(dest): if not qtlib.QuestionMsgBox(_('Confirm Overwrite'), _('The directory "%s" is not empty!\n\n' 'Do you want to overwrite it?') % dest, parent=self): return False else: if os.path.isfile(dest): if not qtlib.QuestionMsgBox(_('Confirm Overwrite'), _('The file "%s" already exists!\n\n' 'Do you want to overwrite it?') % dest, parent=self): return False else: qtlib.WarningMsgBox(_('Duplicate Name'), _('The destination "%s" already exists as ' 'a folder!') % dest) return False # prepare command line cmdline = self.compose_command(hglib.fromunicode(dest), type) if self.files_in_rev_chk.isChecked(): self.savedcwd = os.getcwd() os.chdir(self.repo.root) # start archiving self.cmd.run(cmdline) def detail_clicked(self): if self.cmd.outputShown(): self.cmd.setShowOutput(False) else: self.cmd.setShowOutput(True) def cancel_clicked(self): self.cmd.cancel() def command_started(self): self.dest_edit.setEnabled(False) self.rev_combo.setEnabled(False) self.dest_btn.setEnabled(False) self.files_in_rev_chk.setEnabled(False) self.filesradio.setEnabled(False) self.tarradio.setEnabled(False) self.tbz2radio.setEnabled(False) self.tgzradio.setEnabled(False) self.uzipradio.setEnabled(False) self.zipradio.setEnabled(False) self.cmd.setShown(True) self.arch_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) def command_finished(self, ret): if self.files_in_rev_chk.isChecked(): os.chdir(self.savedcwd) if ret != 0 or self.cmd.outputShown()\ or self.keep_open_chk.isChecked(): if not self.cmd.outputShown(): self.detail_btn.click() self.cancel_btn.setHidden(True) self.close_btn.setShown(True) self.close_btn.setAutoDefault(True) self.close_btn.setFocus() else: self.reject() def command_canceling(self): self.cancel_btn.setDisabled(True) def closeEvent(self, event): self._writesettings() super(ArchiveDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('archive/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('archive/geom', self.saveGeometry()) tortoisehg-2.10/tortoisehg/hgqt/bookmark.py0000644000076400007640000002060112231647662020212 0ustar stevesteve# bookmark.py - Bookmark dialog for TortoiseHg # # Copyright 2010 Michal De Wildt # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, qtlib class BookmarkDialog(QDialog): def __init__(self, repoagent, rev, parent=None): super(BookmarkDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self.rev = rev self.node = repo[rev].node() # base layout box base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(*(0,)*4) base.setSizeConstraint(QLayout.SetFixedSize) self.setLayout(base) box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(8,)*4) self.layout().addLayout(box) ## main layout grid form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) box.addLayout(form) form.addRow(_('Revision:'), QLabel('%d (%s)' % (rev, repo[rev]))) ### bookmark combo self.bookmarkCombo = QComboBox() self.bookmarkCombo.setEditable(True) self.bookmarkCombo.currentIndexChanged.connect(self.bookmarkTextChanged) self.bookmarkCombo.editTextChanged.connect(self.bookmarkTextChanged) qtlib.allowCaseChangingInput(self.bookmarkCombo) form.addRow(_('Bookmark:'), self.bookmarkCombo) ### Rename input self.newNameEdit = QLineEdit() self.newNameEdit.textEdited.connect(self.bookmarkTextChanged) form.addRow(_('New Name:'), self.newNameEdit) ### Activate checkbox self.activateCheckBox = QCheckBox() if self.node == self.repo['.'].node(): self.activateCheckBox.setChecked(True) else: self.activateCheckBox.setChecked(False) self.activateCheckBox.setEnabled(False) form.addRow(_('Activate:'), self.activateCheckBox) ## bottom buttons BB = QDialogButtonBox bbox = QDialogButtonBox() self.addBtn = bbox.addButton(_('&Add'), BB.ActionRole) self.renameBtn = bbox.addButton(_('Re&name'), BB.ActionRole) self.removeBtn = bbox.addButton(_('&Remove'), BB.ActionRole) self.moveBtn = bbox.addButton(_('&Move'), BB.ActionRole) bbox.addButton(BB.Close) bbox.rejected.connect(self.reject) box.addWidget(bbox) self.addBtn.clicked.connect(self.add_bookmark) self.renameBtn.clicked.connect(self.rename_bookmark) self.removeBtn.clicked.connect(self.remove_bookmark) self.moveBtn.clicked.connect(self.move_bookmark) ## horizontal separator self.sep = QFrame() self.sep.setFrameShadow(QFrame.Sunken) self.sep.setFrameShape(QFrame.HLine) self.layout().addWidget(self.sep) ## status line self.status = qtlib.StatusLabel() self.status.setContentsMargins(4, 2, 4, 4) self.layout().addWidget(self.status) self._finishmsg = None # dialog setting self.setWindowTitle(_('Bookmark - %s') % self.repo.displayname) self.setWindowIcon(qtlib.geticon('hg-bookmarks')) # prepare to show self.clear_status() self.refresh() self._repoagent.repositoryChanged.connect(self.refresh) self.bookmarkCombo.setFocus() self.bookmarkTextChanged() @property def repo(self): return self._repoagent.rawRepo() def _allBookmarks(self): return map(hglib.tounicode, self.repo._bookmarks) @pyqtSlot() def refresh(self): """ update display on dialog with recent repo data """ # add bookmarks to drop-down list cur = self.bookmarkCombo.currentText() self.bookmarkCombo.clear() self.bookmarkCombo.addItems(sorted(self._allBookmarks())) if cur: self.bookmarkCombo.setEditText(cur) else: ctx = self.repo[self.rev] cs_bookmarks = ctx.bookmarks() if self.repo._bookmarkcurrent in cs_bookmarks: bm = hglib.tounicode(self.repo._bookmarkcurrent) self.bookmarkCombo.setEditText(bm) elif cs_bookmarks: bm = hglib.tounicode(cs_bookmarks[0]) self.bookmarkCombo.setEditText(bm) else: self.bookmarkTextChanged() @pyqtSlot() def bookmarkTextChanged(self): bookmark = self.bookmarkCombo.currentText() bookmarklocal = hglib.fromunicode(bookmark) if bookmarklocal in self.repo._bookmarks: curnode = self.repo._bookmarks[bookmarklocal] self.addBtn.setEnabled(False) self.newNameEdit.setEnabled(True) self.removeBtn.setEnabled(True) self.renameBtn.setEnabled(bool(self.newNameEdit.text())) self.moveBtn.setEnabled(self.node != curnode) else: self.addBtn.setEnabled(bool(bookmark)) self.removeBtn.setEnabled(False) self.moveBtn.setEnabled(False) self.renameBtn.setEnabled(False) self.newNameEdit.setEnabled(False) def setBookmarkName(self, name): self.bookmarkCombo.setEditText(name) def set_status(self, text, icon=None): self.status.setShown(True) self.sep.setShown(True) self.status.set_status(text, icon) def clear_status(self): self.status.setHidden(True) self.sep.setHidden(True) def _runBookmark(self, *args, **opts): self._finishmsg = opts.pop('finishmsg') cmdline = hglib.buildcmdargs('bookmarks', *args, **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onBookmarkFinished) @pyqtSlot(int) def _onBookmarkFinished(self, ret): if ret == 0: self.bookmarkCombo.clearEditText() self.newNameEdit.setText('') self.set_status(self._finishmsg, True) else: self.set_status(self._cmdsession.errorString(), False) @pyqtSlot() def add_bookmark(self): bookmark = unicode(self.bookmarkCombo.currentText()) if bookmark in self._allBookmarks(): self.set_status(_('A bookmark named "%s" already exists') % bookmark, False) return finishmsg = _("Bookmark '%s' has been added") % bookmark rev = None if not self.activateCheckBox.isChecked(): rev = self.rev self._runBookmark(bookmark, rev=rev, finishmsg=finishmsg) @pyqtSlot() def move_bookmark(self): bookmark = unicode(self.bookmarkCombo.currentText()) if bookmark not in self._allBookmarks(): self.set_status(_('Bookmark named "%s" does not exist') % bookmark, False) return finishmsg = _("Bookmark '%s' has been moved") % bookmark rev = None if not self.activateCheckBox.isChecked(): rev = self.rev self._runBookmark(bookmark, rev=rev, force=True, finishmsg=finishmsg) @pyqtSlot() def remove_bookmark(self): bookmark = unicode(self.bookmarkCombo.currentText()) if bookmark not in self._allBookmarks(): self.set_status(_("Bookmark '%s' does not exist") % bookmark, False) return finishmsg = _("Bookmark '%s' has been removed") % bookmark self._runBookmark(bookmark, delete=True, finishmsg=finishmsg) @pyqtSlot() def rename_bookmark(self): name = unicode(self.bookmarkCombo.currentText()) if name not in self._allBookmarks(): self.set_status(_("Bookmark '%s' does not exist") % name, False) return newname = unicode(self.newNameEdit.text()) if newname in self._allBookmarks(): self.set_status(_('A bookmark named "%s" already exists') % newname, False) return finishmsg = (_("Bookmark '%s' has been renamed to '%s'") % (name, newname)) self._runBookmark(name, newname, rename=True, finishmsg=finishmsg) tortoisehg-2.10/tortoisehg/hgqt/blockmatcher.py0000644000076400007640000003033712215326102021033 0ustar stevesteve# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. """ Qt4 widgets to display diffs as blocks """ import sys, os from PyQt4.QtGui import * from PyQt4.QtCore import * class BlockList(QWidget): """ A simple widget to be 'linked' to the scrollbar of a diff text view. It represents diff blocks with coloured rectangles, showing currently viewed area by a semi-transparant rectangle sliding above them. """ rangeChanged = pyqtSignal(int,int) valueChanged = pyqtSignal(int) pageStepChanged = pyqtSignal(int) def __init__(self, *args): QWidget.__init__(self, *args) self._blocks = set() self._minimum = 0 self._maximum = 100 self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5), '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5), 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5), 's': QColor(0xFF, 0xA5, 0x00, ),#0xa5), } self._sbar = None self._value = 0 self._pagestep = 10 self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25) self._vrectbordercolor = self._vrectcolor.darker() self.sizePolicy().setControlType(QSizePolicy.Slider) self.setMinimumWidth(20) def clear(self): self._blocks = set() def addBlock(self, typ, alo, ahi): self._blocks.add((typ, alo, ahi)) def setMaximum(self, maximum): self._maximum = maximum self.update() self.rangeChanged.emit(self._minimum, self._maximum) def setMinimum(self, minimum): self._minimum = minimum self.update() self.rangeChanged.emit(self._minimum, self._maximum) def setRange(self, minimum, maximum): if minimum == maximum: return self._minimum = minimum self._maximum = maximum self.update() self.rangeChanged.emit(self._minimum, self._maximum) def setValue(self, val): if val != self._value: self._value = val self.update() self.valueChanged.emit(val) def setPageStep(self, pagestep): if pagestep != self._pagestep: self._pagestep = pagestep self.update() self.pageStepChanged.emit(pagestep) def linkScrollBar(self, sbar): """ Make the block list displayer be linked to the scrollbar """ self._sbar = sbar self.setUpdatesEnabled(False) self.setMaximum(sbar.maximum()) self.setMinimum(sbar.minimum()) self.setPageStep(sbar.pageStep()) self.setValue(sbar.value()) self.setUpdatesEnabled(True) sbar.valueChanged.connect(self.setValue) sbar.rangeChanged.connect(self.setRange) self.valueChanged.connect(sbar.setValue) self.rangeChanged.connect(lambda x, y: sbar.setRange(x,y)) self.pageStepChanged.connect(lambda x: sbar.setPageStep(x)) def syncPageStep(self): self.setPageStep(self._sbar.pageStep()) def paintEvent(self, event): w = self.width() - 1 h = self.height() p = QPainter(self) p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep)) p.setPen(Qt.NoPen) for typ, alo, ahi in self._blocks: p.save() p.setBrush(self.blockTypes[typ]) p.drawRect(1, alo, w-1, ahi-alo) p.restore() p.save() p.setPen(self._vrectbordercolor) p.setBrush(self._vrectcolor) p.drawRect(0, self._value, w, self._pagestep) p.restore() def scrollToPos(self, y): # Scroll to the position which specified by Y coodinate. if not isinstance(self._sbar, QScrollBar): return ratio = float(y) / self.height() minimum, maximum, step = self._minimum, self._maximum, self._pagestep value = minimum + (maximum + step - minimum) * ratio - (step * 0.5) value = min(maximum, max(minimum, value)) # round to valid range. self.setValue(value) def mousePressEvent(self, event): super(BlockList, self).mousePressEvent(event) self.scrollToPos(event.y()) def mouseMoveEvent(self, event): super(BlockList, self).mouseMoveEvent(event) self.scrollToPos(event.y()) class BlockMatch(BlockList): """ A simpe widget to be linked to 2 file views (text areas), displaying 2 versions of a same file (diff). It will show graphically matching diff blocks between the 2 text areas. """ rangeChanged = pyqtSignal(int, int, QString) valueChanged = pyqtSignal(int, QString) pageStepChanged = pyqtSignal(int, QString) def __init__(self, *args): QWidget.__init__(self, *args) self._blocks = set() self._minimum = {'left': 0, 'right': 0} self._maximum = {'left': 100, 'right': 100} self.blockTypes = {'+': QColor(0xA0, 0xFF, 0xB0, ),#0xa5), '-': QColor(0xFF, 0xA0, 0xA0, ),#0xa5), 'x': QColor(0xA0, 0xA0, 0xFF, ),#0xa5), } self._sbar = {} self._value = {'left': 0, 'right': 0} self._pagestep = {'left': 10, 'right': 10} self._vrectcolor = QColor(0x00, 0x00, 0x55, 0x25) self._vrectbordercolor = self._vrectcolor.darker() self.sizePolicy().setControlType(QSizePolicy.Slider) self.setMinimumWidth(20) def nDiffs(self): return len(self._blocks) def showDiff(self, delta): ps_l = float(self._pagestep['left']) ps_r = float(self._pagestep['right']) mv_l = self._value['left'] mv_r = self._value['right'] Mv_l = mv_l + ps_l Mv_r = mv_r + ps_r vblocks = [] blocks = sorted(self._blocks, key=lambda x:(x[1],x[3],x[2],x[4])) for i, (typ, alo, ahi, blo, bhi) in enumerate(blocks): if (mv_l<=alo<=Mv_l or mv_l<=ahi<=Mv_l or mv_r<=blo<=Mv_r or mv_r<=bhi<=Mv_r): break else: i = -1 i += delta if i < 0: return -1 if i >= len(blocks): return 1 typ, alo, ahi, blo, bhi = blocks[i] self.setValue(alo, "left") self.setValue(blo, "right") if i == 0: return -1 if i == len(blocks)-1: return 1 return 0 def nextDiff(self): return self.showDiff(+1) def prevDiff(self): return self.showDiff(-1) def addBlock(self, typ, alo, ahi, blo=None, bhi=None): if bhi is None: bhi = ahi if blo is None: blo = alo self._blocks.add((typ, alo, ahi, blo, bhi)) def paintEvent(self, event): if self._pagestep['left'] == 0 or self._pagestep['right'] == 0: return w = self.width() h = self.height() p = QPainter(self) p.setRenderHint(p.Antialiasing) ps_l = float(self._pagestep['left']) ps_r = float(self._pagestep['right']) v_l = self._value['left'] v_r = self._value['right'] # we do integer divisions here cause the pagestep is the # integer number of fully displayed text lines scalel = self._sbar['left'].height()//ps_l scaler = self._sbar['right'].height()//ps_r ml = v_l Ml = v_l + ps_l mr = v_r Mr = v_r + ps_r p.setPen(Qt.NoPen) for typ, alo, ahi, blo, bhi in self._blocks: if not (ml<=alo<=Ml or ml<=ahi<=Ml or mr<=blo<=Mr or mr<=bhi<=Mr): continue p.save() p.setBrush(self.blockTypes[typ]) path = QPainterPath() path.moveTo(0, scalel * (alo - ml)) path.cubicTo(w/3.0, scalel * (alo - ml), 2*w/3.0, scaler * (blo - mr), w, scaler * (blo - mr)) path.lineTo(w, scaler * (bhi - mr) + 2) path.cubicTo(2*w/3.0, scaler * (bhi - mr) + 2, w/3.0, scalel * (ahi - ml) + 2, 0, scalel * (ahi - ml) + 2) path.closeSubpath() p.drawPath(path) p.restore() def setMaximum(self, maximum, side): self._maximum[side] = maximum self.update() self.rangeChanged.emit(self._minimum[side], self._maximum[side], side) def setMinimum(self, minimum, side): self._minimum[side] = minimum self.update() self.rangeChanged.emit(self._minimum[side], self._maximum[side], side) def setRange(self, minimum, maximum, side=None): if side is None: if self.sender() == self._sbar['left']: side = 'left' else: side = 'right' self._minimum[side] = minimum self._maximum[side] = maximum self.update() self.rangeChanged.emit(self._minimum[side], self._maximum[side], side) def setValue(self, val, side=None): if side is None: if self.sender() == self._sbar['left']: side = 'left' else: side = 'right' if val != self._value[side]: self._value[side] = val self.update() self.valueChanged.emit(val, side) def setPageStep(self, pagestep, side): if pagestep != self._pagestep[side]: self._pagestep[side] = pagestep self.update() self.pageStepChanged.emit(pagestep, side) @pyqtSlot() def syncPageStep(self): for side in ['left', 'right']: self.setPageStep(self._sbar[side].pageStep(), side) def linkScrollBar(self, sb, side): """ Make the block list displayer be linked to the scrollbar """ if self._sbar is None: self._sbar = {} self._sbar[side] = sb self.setUpdatesEnabled(False) self.setMaximum(sb.maximum(), side) self.setMinimum(sb.minimum(), side) self.setPageStep(sb.pageStep(), side) self.setValue(sb.value(), side) self.setUpdatesEnabled(True) sb.valueChanged.connect(self.setValue) sb.rangeChanged.connect(self.setRange) self.valueChanged.connect(lambda v, s: side==s and sb.setValue(v)) self.rangeChanged.connect( lambda v1, v2, s: side==s and sb.setRange(v1, v2)) self.pageStepChanged.connect( lambda v, s: side==s and sb.setPageStep(v)) if __name__ == '__main__': a = QApplication([]) f = QFrame() l = QHBoxLayout(f) sb1 = QScrollBar() sb2 = QScrollBar() w0 = BlockList() w0.addBlock('-', 200, 300) w0.addBlock('-', 450, 460) w0.addBlock('x', 500, 501) w0.linkScrollBar(sb1) w1 = BlockMatch() w1.addBlock('+', 12, 42) w1.addBlock('+', 55, 142) w1.addBlock('-', 200, 300) w1.addBlock('-', 330, 400, 450, 460) w1.addBlock('x', 420, 450, 500, 501) w1.linkScrollBar(sb1, 'left') w1.linkScrollBar(sb2, 'right') w2 = BlockList() w2.addBlock('+', 12, 42) w2.addBlock('+', 55, 142) w2.addBlock('x', 420, 450) w2.linkScrollBar(sb2) l.addWidget(sb1) l.addWidget(w0) l.addWidget(w1) l.addWidget(w2) l.addWidget(sb2) w0.setRange(0, 1200) w0.setPageStep(100) w1.setRange(0, 1200, 'left') w1.setRange(0, 1200, 'right') w1.setPageStep(100, 'left') w1.setPageStep(100, 'right') w2.setRange(0, 1200) w2.setPageStep(100) print "sb1=", sb1.minimum(), sb1.maximum(), sb1.pageStep() print "sb2=", sb2.minimum(), sb2.maximum(), sb2.pageStep() f.show() a.exec_() tortoisehg-2.10/tortoisehg/hgqt/postreview_ui.py0000644000076400007640000002527412212224146021307 0ustar stevesteve# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/steve/repos/thg/tortoisehg/hgqt/postreview.ui' # # Created: Thu Sep 5 19:57:10 2013 # by: PyQt4 UI code generator 4.6.2 # # WARNING! All changes made in this file will be lost! from tortoisehg.hgqt.i18n import _ from PyQt4 import QtCore, QtGui class Ui_PostReviewDialog(object): def setupUi(self, PostReviewDialog): PostReviewDialog.setObjectName("PostReviewDialog") PostReviewDialog.resize(660, 459) self.verticalLayout_5 = QtGui.QVBoxLayout(PostReviewDialog) self.verticalLayout_5.setObjectName("verticalLayout_5") self.verticalLayout = QtGui.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.tab_widget = QtGui.QTabWidget(PostReviewDialog) self.tab_widget.setMaximumSize(QtCore.QSize(16777215, 110)) self.tab_widget.setObjectName("tab_widget") self.post_review_tab = QtGui.QWidget() self.post_review_tab.setObjectName("post_review_tab") self.formLayout_2 = QtGui.QFormLayout(self.post_review_tab) self.formLayout_2.setObjectName("formLayout_2") self.repo_id_label = QtGui.QLabel(self.post_review_tab) self.repo_id_label.setObjectName("repo_id_label") self.formLayout_2.setWidget(0, QtGui.QFormLayout.LabelRole, self.repo_id_label) self.repo_id_combo = QtGui.QComboBox(self.post_review_tab) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.repo_id_combo.sizePolicy().hasHeightForWidth()) self.repo_id_combo.setSizePolicy(sizePolicy) self.repo_id_combo.setEditable(False) self.repo_id_combo.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.repo_id_combo.setObjectName("repo_id_combo") self.formLayout_2.setWidget(0, QtGui.QFormLayout.FieldRole, self.repo_id_combo) self.summary_label = QtGui.QLabel(self.post_review_tab) self.summary_label.setObjectName("summary_label") self.formLayout_2.setWidget(1, QtGui.QFormLayout.LabelRole, self.summary_label) self.summary_edit = QtGui.QComboBox(self.post_review_tab) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.summary_edit.sizePolicy().hasHeightForWidth()) self.summary_edit.setSizePolicy(sizePolicy) self.summary_edit.setEditable(True) self.summary_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.summary_edit.setObjectName("summary_edit") self.formLayout_2.setWidget(1, QtGui.QFormLayout.FieldRole, self.summary_edit) self.tab_widget.addTab(self.post_review_tab, "") self.update_review_tab = QtGui.QWidget() self.update_review_tab.setObjectName("update_review_tab") self.formLayout_3 = QtGui.QFormLayout(self.update_review_tab) self.formLayout_3.setObjectName("formLayout_3") self.review_id_label = QtGui.QLabel(self.update_review_tab) self.review_id_label.setObjectName("review_id_label") self.formLayout_3.setWidget(0, QtGui.QFormLayout.LabelRole, self.review_id_label) self.review_id_combo = QtGui.QComboBox(self.update_review_tab) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.review_id_combo.sizePolicy().hasHeightForWidth()) self.review_id_combo.setSizePolicy(sizePolicy) self.review_id_combo.setEditable(False) self.review_id_combo.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.review_id_combo.setObjectName("review_id_combo") self.formLayout_3.setWidget(0, QtGui.QFormLayout.FieldRole, self.review_id_combo) self.update_fields = QtGui.QCheckBox(self.update_review_tab) self.update_fields.setObjectName("update_fields") self.formLayout_3.setWidget(1, QtGui.QFormLayout.FieldRole, self.update_fields) self.tab_widget.addTab(self.update_review_tab, "") self.verticalLayout.addWidget(self.tab_widget) self.options_group = QtGui.QGroupBox(PostReviewDialog) self.options_group.setObjectName("options_group") self.gridLayout = QtGui.QGridLayout(self.options_group) self.gridLayout.setObjectName("gridLayout") self.outgoing_changes_check = QtGui.QCheckBox(self.options_group) self.outgoing_changes_check.setObjectName("outgoing_changes_check") self.gridLayout.addWidget(self.outgoing_changes_check, 0, 0, 1, 1) self.branch_check = QtGui.QCheckBox(self.options_group) self.branch_check.setObjectName("branch_check") self.gridLayout.addWidget(self.branch_check, 0, 1, 1, 1) self.publish_immediately_check = QtGui.QCheckBox(self.options_group) self.publish_immediately_check.setObjectName("publish_immediately_check") self.gridLayout.addWidget(self.publish_immediately_check, 2, 0, 1, 1) self.verticalLayout.addWidget(self.options_group) self.changesets_box = QtGui.QGroupBox(PostReviewDialog) self.changesets_box.setEnabled(True) self.changesets_box.setObjectName("changesets_box") self.verticalLayout_3 = QtGui.QVBoxLayout(self.changesets_box) self.verticalLayout_3.setObjectName("verticalLayout_3") self.changesets_view = QtGui.QTreeView(self.changesets_box) self.changesets_view.setIndentation(0) self.changesets_view.setRootIsDecorated(False) self.changesets_view.setItemsExpandable(False) self.changesets_view.setObjectName("changesets_view") self.changesets_view.header().setHighlightSections(False) self.changesets_view.header().setSortIndicatorShown(False) self.verticalLayout_3.addWidget(self.changesets_view) self.verticalLayout.addWidget(self.changesets_box) self.verticalLayout_5.addLayout(self.verticalLayout) self.dialogbuttons_layout = QtGui.QHBoxLayout() self.dialogbuttons_layout.setObjectName("dialogbuttons_layout") self.settings_button = QtGui.QPushButton(PostReviewDialog) self.settings_button.setDefault(False) self.settings_button.setObjectName("settings_button") self.dialogbuttons_layout.addWidget(self.settings_button) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem) self.progress_bar = QtGui.QProgressBar(PostReviewDialog) self.progress_bar.setMinimumSize(QtCore.QSize(200, 0)) font = QtGui.QFont() self.progress_bar.setFont(font) self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(0) self.progress_bar.setProperty("value", -1) self.progress_bar.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) self.progress_bar.setTextVisible(True) self.progress_bar.setOrientation(QtCore.Qt.Horizontal) self.progress_bar.setInvertedAppearance(False) self.progress_bar.setTextDirection(QtGui.QProgressBar.TopToBottom) self.progress_bar.setObjectName("progress_bar") self.dialogbuttons_layout.addWidget(self.progress_bar) self.progress_label = QtGui.QLabel(PostReviewDialog) self.progress_label.setObjectName("progress_label") self.dialogbuttons_layout.addWidget(self.progress_label) spacerItem1 = QtGui.QSpacerItem(0, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem1) self.post_review_button = QtGui.QPushButton(PostReviewDialog) self.post_review_button.setEnabled(False) self.post_review_button.setDefault(False) self.post_review_button.setObjectName("post_review_button") self.dialogbuttons_layout.addWidget(self.post_review_button) self.close_button = QtGui.QPushButton(PostReviewDialog) self.close_button.setEnabled(True) self.close_button.setDefault(True) self.close_button.setObjectName("close_button") self.dialogbuttons_layout.addWidget(self.close_button) self.verticalLayout_5.addLayout(self.dialogbuttons_layout) self.repo_id_label.setBuddy(self.repo_id_combo) self.summary_label.setBuddy(self.summary_edit) self.review_id_label.setBuddy(self.review_id_combo) self.retranslateUi(PostReviewDialog) self.tab_widget.setCurrentIndex(0) QtCore.QObject.connect(self.post_review_button, QtCore.SIGNAL("clicked()"), PostReviewDialog.accept) QtCore.QObject.connect(self.settings_button, QtCore.SIGNAL("clicked()"), PostReviewDialog.onSettingsButtonClicked) QtCore.QObject.connect(self.close_button, QtCore.SIGNAL("clicked()"), PostReviewDialog.close) QtCore.QObject.connect(self.outgoing_changes_check, QtCore.SIGNAL("toggled(bool)"), PostReviewDialog.outgoingChangesCheckToggle) QtCore.QObject.connect(self.branch_check, QtCore.SIGNAL("toggled(bool)"), PostReviewDialog.branchCheckToggle) QtCore.QObject.connect(self.tab_widget, QtCore.SIGNAL("currentChanged(int)"), PostReviewDialog.tabChanged) QtCore.QMetaObject.connectSlotsByName(PostReviewDialog) PostReviewDialog.setTabOrder(self.changesets_view, self.post_review_button) PostReviewDialog.setTabOrder(self.post_review_button, self.settings_button) def retranslateUi(self, PostReviewDialog): PostReviewDialog.setWindowTitle(_('Review Board')) self.repo_id_label.setText(_('Repository ID:')) self.summary_label.setText(_('Summary:')) self.tab_widget.setTabText(self.tab_widget.indexOf(self.post_review_tab), _('Post Review')) self.review_id_label.setText(_('Review ID:')) self.update_fields.setText(_('Update the fields of this existing request')) self.tab_widget.setTabText(self.tab_widget.indexOf(self.update_review_tab), _('Update Review')) self.options_group.setTitle(_('Options')) self.outgoing_changes_check.setText(_('Create diff with all outgoing changes')) self.branch_check.setText(_('Create diff with all changes on this branch')) self.publish_immediately_check.setText(_('Publish request immediately')) self.changesets_box.setTitle(_('Changesets')) self.settings_button.setText(_('&Settings')) self.progress_bar.setFormat(_('%p%')) self.progress_label.setText(_('Connecting to Review Board...')) self.post_review_button.setText(_('Post &Review')) self.close_button.setText(_('&Close')) tortoisehg-2.10/tortoisehg/hgqt/thgimport.py0000644000076400007640000003010212231647662020417 0ustar stevesteve# thgimport.py - Import dialog for TortoiseHg # # Copyright 2009 Yuki KODAMA # Copyright 2010 David Wilhelm # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import shutil import tempfile from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, cslist, qtlib, commit _FILE_FILTER = "%s;;%s" % (_("Patch files (*.diff *.patch)"), _("All files (*)")) # TODO: handle --mq options from command line or MQ widget class ImportDialog(QDialog): """Dialog to import patches""" patchImported = pyqtSignal() def __init__(self, repoagent, parent, **opts): super(ImportDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self.setWindowIcon(qtlib.geticon('hg-import')) self.tempfiles = [] self._repoagent = repoagent # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid self.grid = grid = QGridLayout() grid.setSpacing(6) box.addLayout(grid) ### source input self.src_combo = QComboBox() self.src_combo.setEditable(True) self.src_combo.setMinimumWidth(310) self.file_btn = QPushButton(_('Browse...')) self.file_btn.setAutoDefault(False) self.file_btn.clicked.connect(self.browsefiles) self.dir_btn = QPushButton(_('Browse Directory...')) self.dir_btn.setAutoDefault(False) self.dir_btn.clicked.connect(self.browsedir) self.clip_btn = QPushButton(_('Import from Clipboard')) self.clip_btn.setAutoDefault(False) self.clip_btn.clicked.connect(self.getcliptext) grid.addWidget(QLabel(_('Source:')), 0, 0) grid.addWidget(self.src_combo, 0, 1) srcbox = QHBoxLayout() srcbox.addWidget(self.file_btn) srcbox.addWidget(self.dir_btn) srcbox.addWidget(self.clip_btn) grid.addLayout(srcbox, 1, 1) self.p0chk = QCheckBox(_('Do not strip paths (-p0), ' 'required for SVN patches')) grid.addWidget(self.p0chk, 2, 1, Qt.AlignLeft) ### patch list self.cslist = cslist.ChangesetList(self.repo) cslistrow = 4 cslistcol = 1 grid.addWidget(self.cslist, cslistrow, cslistcol) grid.addWidget(QLabel(_('Preview:')), 3, 0, Qt.AlignLeft | Qt.AlignTop) statbox = QHBoxLayout() self.status = QLabel("") statbox.addWidget(self.status) self.targetcombo = QComboBox() self.targetcombo.addItem(_('Repository'), ('import',)) self.targetcombo.addItem(_('Shelf'), ('copy',)) self.targetcombo.addItem(_('Working Directory'), ('import', '--no-commit')) cur = self.repo.thgactivemqname if cur: self.targetcombo.addItem(hglib.tounicode(cur), ('qimport',)) self.targetcombo.currentIndexChanged.connect(self._updatep0chk) statbox.addWidget(self.targetcombo) grid.addItem(statbox, 3, 1) ## command widget self.cmd = cmdui.Widget(True, False, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandCanceling.connect(self.command_canceling) grid.setRowStretch(cslistrow, 1) grid.setColumnStretch(cslistcol, 1) box.addWidget(self.cmd) self.stlabel = QLabel(_('Checking working directory status...')) self.stlabel.linkActivated.connect(self.commitActivated) box.addWidget(self.stlabel) QTimer.singleShot(0, self.checkStatus) ## bottom buttons buttons = QDialogButtonBox() self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel) self.cancel_btn.clicked.connect(self.cancel_clicked) self.close_btn = buttons.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.reject) self.close_btn.setAutoDefault(False) self.import_btn = buttons.addButton(_('&Import'), QDialogButtonBox.ActionRole) self.import_btn.clicked.connect(self.thgimport) self.detail_btn = buttons.addButton(_('Detail'), QDialogButtonBox.ResetRole) self.detail_btn.setAutoDefault(False) self.detail_btn.setCheckable(True) self.detail_btn.toggled.connect(self.detail_toggled) box.addWidget(buttons) # signal handlers self.src_combo.editTextChanged.connect(self.preview) self.src_combo.lineEdit().returnPressed.connect(self.thgimport) self.p0chk.toggled.connect(self.preview) # dialog setting self.setLayout(box) self.layout().setSizeConstraint(QLayout.SetMinAndMaxSize) self.setWindowTitle(_('Import - %s') % self.repo.displayname) #self.setWindowIcon(qtlib.geticon('import')) # prepare to show self.src_combo.lineEdit().selectAll() self.cmd.setHidden(True) self.cancel_btn.setHidden(True) self.detail_btn.setHidden(True) self._updatep0chk() self.preview() ### Private Methods ### @property def repo(self): return self._repoagent.rawRepo() def commitActivated(self): dlg = commit.CommitDialog(self._repoagent, [], {}, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.checkStatus() def checkStatus(self): self.repo.dirstate.invalidate() wctx = self.repo[None] M, A, R = wctx.status()[:3] if M or A or R: text = _('Working directory is not clean! ' 'View changes...') self.stlabel.setText(text) else: self.stlabel.clear() def keyPressEvent(self, event): if event.matches(QKeySequence.Refresh): self.checkStatus() else: return super(ImportDialog, self).keyPressEvent(event) def browsefiles(self): caption = _("Select patches") filelist = QFileDialog.getOpenFileNames(parent=self, caption=caption, directory=self.repo.root, filter=_FILE_FILTER) if filelist: # Qt file browser uses '/' in paths, even on Windows. nl = QStringList([QDir.toNativeSeparators(x) for x in filelist]) self.src_combo.setEditText(nl.join(os.pathsep)) self.src_combo.setFocus() def browsedir(self): caption = _("Select Directory containing patches") path = QFileDialog.getExistingDirectory(parent=self, directory=self.repo.root, caption=caption) if path: self.src_combo.setEditText(QDir.toNativeSeparators(path)) self.src_combo.setFocus() def getcliptext(self): mdata = QApplication.clipboard().mimeData() if mdata.hasFormat('text/x-diff'): # lossless text = str(mdata.data('text/x-diff')) elif mdata.hasText(): # could be encoding damaged text = hglib.fromunicode(mdata.text(), errors='ignore') else: return filename = self.writetempfile(text) curtext = self.src_combo.currentText() if curtext: self.src_combo.setEditText(curtext + os.pathsep + filename) else: self.src_combo.setEditText(filename) def _targetcommand(self): index = self.targetcombo.currentIndex() return self.targetcombo.itemData(index).toPyObject() @pyqtSlot() def _updatep0chk(self): cmd = self._targetcommand()[0] self.p0chk.setEnabled(cmd == 'import') if not self.p0chk.isEnabled(): self.p0chk.setChecked(False) def updatestatus(self): items = self.cslist.curitems count = items and len(items) or 0 countstr = qtlib.markup(_("%s patches") % count, weight='bold') if count: self.targetcombo.setVisible(True) text = _('%s will be imported to ') % countstr else: self.targetcombo.setVisible(False) text = qtlib.markup(_('Nothing to import'), weight='bold', fg='red') self.status.setText(text) def preview(self): patches = self.getfilepaths() if not patches: self.cslist.clear() self.import_btn.setDisabled(True) else: self.cslist.update([os.path.abspath(p) for p in patches]) self.import_btn.setEnabled(True) self.updatestatus() def getfilepaths(self): src = hglib.fromunicode(self.src_combo.currentText()) if not src: return [] files = [] for path in src.split(os.pathsep): path = path.strip('\r\n\t ') if not os.path.exists(path) or path in files: continue if os.path.isfile(path): files.append(path) elif os.path.isdir(path): entries = os.listdir(path) for entry in sorted(entries): _file = os.path.join(path, entry) if os.path.isfile(_file) and not _file in files: files.append(_file) return files def setfilepaths(self, paths): """Set file paths of patches to import; paths is in locale encoding""" self.src_combo.setEditText( os.pathsep.join(hglib.tounicode(p) for p in paths)) def thgimport(self): if self.cslist.curitems is None: return cmdline = list(self._targetcommand()) if cmdline == ['copy']: # import to shelf existing = self.repo.thgshelves() if not os.path.exists(self.repo.shelfdir): os.mkdir(self.repo.shelfdir) for file in self.cslist.curitems: shutil.copy(file, self.repo.shelfdir) return cmdline.extend(['--repository', self.repo.root]) if self.p0chk.isChecked(): cmdline.append('-p0') cmdline.extend(['--verbose', '--']) cmdline.extend(self.cslist.curitems) self.repo.incrementBusyCount() self.cmd.run(cmdline) def writetempfile(self, text): fd, filename = tempfile.mkstemp(suffix='.patch', prefix='thg-import-') try: os.write(fd, text) finally: os.close(fd) self.tempfiles.append(filename) return filename def unlinktempfiles(self): for path in self.tempfiles: os.unlink(path) ### Override Handlers ### def accept(self): self.unlinktempfiles() super(ImportDialog, self).accept() def reject(self): self.unlinktempfiles() super(ImportDialog, self).reject() ### Signal Handlers ### def cancel_clicked(self): self.cmd.cancel() self.reject() def detail_toggled(self, checked): self.cmd.setShowOutput(checked) def command_started(self): self.cmd.setShown(True) self.import_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) def command_finished(self, ret): self.repo.decrementBusyCount() if ret == 0: self.patchImported.emit() if ret != 0 or self.cmd.outputShown(): self.detail_btn.setChecked(True) self.close_btn.setShown(True) self.close_btn.setAutoDefault(True) self.close_btn.setFocus() self.cancel_btn.setHidden(True) self.import_btn.setHidden(False) else: self.accept() def command_canceling(self): self.cancel_btn.setDisabled(True) tortoisehg-2.10/tortoisehg/hgqt/filedialogs.py0000644000076400007640000011024112231647662020667 0ustar stevesteve# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. """ Qt4 dialogs to display hg revisions of a file """ import os import difflib from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, visdiff, filerevmodel, blockmatcher, lexers from tortoisehg.hgqt import fileview, repoview, revpanel, revert from tortoisehg.hgqt.qscilib import Scintilla from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciScintilla sides = ('left', 'right') otherside = {'left': 'right', 'right': 'left'} _MARKERPLUSLINE = 31 _MARKERMINUSLINE = 30 _MARKERPLUSUNDERLINE = 29 _MARKERMINUSUNDERLINE = 28 _colormap = { '+': QColor(0xA0, 0xFF, 0xB0), '-': QColor(0xFF, 0xA0, 0xA0), 'x': QColor(0xA0, 0xA0, 0xFF) } class _FileDiffScintilla(Scintilla): def paintEvent(self, event): super(_FileDiffScintilla, self).paintEvent(event) viewport = self.viewport() start = self.firstVisibleLine() scale = self.textHeight(0) # Currently all lines are the same height n = min(viewport.height() / scale + 1, self.lines() - start) lines = [] for i in xrange(0, n): m = self.markersAtLine(start + i) if m & (1 << _MARKERPLUSLINE): lines.append((i, _colormap['+'], )) if m & (1 << _MARKERPLUSUNDERLINE): lines.append((i + 1, _colormap['+'], )) if m & (1 << _MARKERMINUSLINE): lines.append((i, _colormap['-'], )) if m & (1 << _MARKERMINUSUNDERLINE): lines.append((i + 1, _colormap['-'], )) p = QPainter(viewport) p.setRenderHint(QPainter.Antialiasing) for (line, color) in lines: p.setPen(QPen(color, 3.0)) y = line * scale p.drawLine(0, y, viewport.width(), y) class _AbstractFileDialog(QMainWindow): finished = pyqtSignal(int) def __init__(self, repoagent, filename): QMainWindow.__init__(self) self._repoagent = repoagent self.setupUi(self) self._show_rev = None assert not isinstance(filename, (unicode, QString)) self.filename = filename repo = repoagent.rawRepo() self.setWindowTitle(_('Hg file log viewer [%s] - %s') % (repo.displayname, hglib.tounicode(filename))) self.setWindowIcon(qtlib.geticon('hg-log')) self.createActions() self.setupToolbars() self.setupViews() self.setupModels() def closeEvent(self, event): super(_AbstractFileDialog, self).closeEvent(event) self.finished.emit(0) # mimic QDialog exit @property def repo(self): return self._repoagent.rawRepo() def reload(self): 'Reload toolbar action handler' self.repo.thginvalidate() self.setupModels() def onRevisionActivated(self, rev): """ Callback called when a revision is double-clicked in the revisions table """ # TODO: implement by using signal-slot if possible from tortoisehg.hgqt import run run.qtrun.showRepoInWorkbench(hglib.tounicode(self.repo.root), rev) class FileLogDialog(_AbstractFileDialog): """ A dialog showing a revision graph for a file. """ def __init__(self, repoagent, filename): super(FileLogDialog, self).__init__(repoagent, filename) self._readSettings() self.menu = None self.dualmenu = None self.revdetails = None def closeEvent(self, event): self._writeSettings() super(FileLogDialog, self).closeEvent(event) def _readSettings(self): s = QSettings() s.beginGroup('filelog') try: self.textView.loadSettings(s, 'fileview') self.restoreGeometry(s.value('geom').toByteArray()) self.splitter.restoreState(s.value('splitter').toByteArray()) self.revpanel.set_expanded(s.value('revpanel.expanded').toBool()) finally: s.endGroup() def _writeSettings(self): s = QSettings() s.beginGroup('filelog') try: self.textView.saveSettings(s, 'fileview') s.setValue('revpanel.expanded', self.revpanel.is_expanded()) s.setValue('geom', self.saveGeometry()) s.setValue('splitter', self.splitter.saveState()) finally: s.endGroup() def setupUi(self, o): self.editToolbar = QToolBar(self) self.editToolbar.setContextMenuPolicy(Qt.PreventContextMenu) self.addToolBar(Qt.ToolBarArea(Qt.TopToolBarArea), self.editToolbar) self.actionClose = QAction(self) self.actionClose.setShortcuts(QKeySequence.Close) self.actionReload = QAction(self) self.actionReload.setShortcuts(QKeySequence.Refresh) self.editToolbar.addAction(self.actionReload) self.addAction(self.actionClose) self.splitter = QSplitter(Qt.Vertical) self.setCentralWidget(self.splitter) cs = ('fileLogDialog', _('File History Log Columns')) self.repoview = repoview.HgRepoView(self.repo, cs[0], cs, self.splitter) self.repoview.revisionSelectionChanged.connect( self._checkValidSelection) self.contentframe = QFrame(self.splitter) vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setMargin(0) self.contentframe.setLayout(vbox) self.revpanel = revpanel.RevPanelWidget(self.repo) self.revpanel.linkActivated.connect(self.onLinkActivated) vbox.addWidget(self.revpanel, 0) self.textView = fileview.HgFileView(self._repoagent, self) self.textView.revisionSelected.connect(self.goto) vbox.addWidget(self.textView, 1) def setupViews(self): self.textView.showMessage.connect(self.statusBar().showMessage) def setupToolbars(self): self.editToolbar.addSeparator() self.editToolbar.addAction(self.actionBack) self.editToolbar.addAction(self.actionForward) def setupModels(self): self.filerevmodel = filerevmodel.FileRevModel(self.repo, self.repoview.colselect[0], parent=self) self.repoview.setModel(self.filerevmodel) self.repoview.revisionSelected.connect(self.onRevisionSelected) self.repoview.revisionActivated.connect(self.onRevisionActivated) self.repoview.menuRequested.connect(self.viewMenuRequest) self.filerevmodel.showMessage.connect(self.statusBar().showMessage) self.filerevmodel.filled.connect(self.modelFilled) self.filerevmodel.setFilename(self.filename) def createActions(self): self.actionClose.triggered.connect(self.close) self.actionReload.triggered.connect(self.reload) self.actionReload.setIcon(qtlib.geticon('view-refresh')) self.actionBack = QAction(_('Back'), self, enabled=False, icon=qtlib.geticon('go-previous')) self.actionForward = QAction(_('Forward'), self, enabled=False, icon=qtlib.geticon('go-next')) self.repoview.revisionSelected.connect(self._updateHistoryActions) self.actionBack.triggered.connect(self.repoview.back) self.actionForward.triggered.connect(self.repoview.forward) @pyqtSlot() def _updateHistoryActions(self): self.actionBack.setEnabled(self.repoview.canGoBack()) self.actionForward.setEnabled(self.repoview.canGoForward()) def modelFilled(self): self.repoview.resizeColumns() if self._show_rev is not None: index = self.filerevmodel.indexLinkedFromRev(self._show_rev) self._show_rev = None elif self.repoview.currentIndex().isValid(): return # already set by goto() else: index = self.filerevmodel.index(0,0) if index is not None: self.repoview.setCurrentIndex(index) @pyqtSlot(QPoint, object) def viewMenuRequest(self, point, selection): 'User requested a context menu in repo view widget' if not selection or len(selection) > 2: return if len(selection) == 2: if self.dualmenu is None: self.dualmenu = menu = QMenu(self) a = menu.addAction(_('&Diff Selected Changesets')) a.triggered.connect(self.onVisualDiffRevs) a = menu.addAction(_('Diff Selected &File Revisions')) a.setIcon(qtlib.geticon('visualdiff')) a.triggered.connect(self.onVisualDiffFileRevs) else: menu = self.dualmenu elif self.menu is None: self.menu = menu = QMenu(self) a = menu.addAction(_('&Diff Changeset to Parent')) a.setIcon(qtlib.geticon('visualdiff')) a.triggered.connect(self.onVisualDiff) a = menu.addAction(_('Diff Changeset to &Local')) a.setIcon(qtlib.geticon('ldiff')) a.triggered.connect(self.onVisualDiffToLocal) menu.addSeparator() a = menu.addAction(_('Diff &File to Parent')) a.setIcon(qtlib.geticon('visualdiff')) a.triggered.connect(self.onVisualDiffFile) a = menu.addAction(_('Diff File to Lo&cal')) a.setIcon(qtlib.geticon('ldiff')) a.triggered.connect(self.onVisualDiffFileToLocal) menu.addSeparator() a = menu.addAction(_('&View at Revision')) a.setIcon(qtlib.geticon('view-at-revision')) a.triggered.connect(self.onViewFileAtRevision) a = menu.addAction(_('&Save at Revision...')) a.triggered.connect(self.onSaveFileAtRevision) a = menu.addAction(_('&Edit Local')) a.setIcon(qtlib.geticon('edit-file')) a.triggered.connect(self.onEditLocal) a = menu.addAction(_('&Revert to Revision...')) a.setIcon(qtlib.geticon('hg-revert')) a.triggered.connect(self.onRevertFileToRevision) menu.addSeparator() a = menu.addAction(_('Show Revision &Details')) a.setIcon(qtlib.geticon('hg-log')) a.triggered.connect(self.onShowRevisionDetails) else: menu = self.menu self.selection = selection menu.exec_(point) def onVisualDiff(self): opts = dict(change=self.selection[0]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def onVisualDiffToLocal(self): opts = dict(rev=['rev(%d)' % self.selection[0]]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def onVisualDiffRevs(self): revs = self.selection if len(revs) != 2: self.textView.showMessage.emit(_('You must select two revisions to diff')) return opts = dict(rev=revs) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def onVisualDiffFile(self): rev = self.selection[0] paths = [self.filerevmodel.graph.filename(rev)] opts = dict(change=self.selection[0]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def onVisualDiffFileToLocal(self): rev = self.selection[0] paths = [self.filerevmodel.graph.filename(rev)] opts = dict(rev=['rev(%d)' % rev]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def onVisualDiffFileRevs(self): revs = self.selection if len(revs) != 2: self.textView.showMessage.emit(_('You must select two revisions to diff')) return paths = [self.filerevmodel.graph.filename(revs[0])] opts = dict(rev=['rev(%d)' % rev for rev in revs]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def onEditLocal(self): filenames = [self.filename] if not filenames: return qtlib.editfiles(self.repo, filenames, parent=self) def onRevertFileToRevision(self): rev = self.selection[0] if rev is None: rev = self.repo['.'].rev() fileSelection = [self.filerevmodel.graph.filename(rev)] if len(fileSelection) == 0: return dlg = revert.RevertDialog(self._repoagent, fileSelection, rev, self) if dlg: dlg.exec_() dlg.deleteLater() def onViewFileAtRevision(self): rev = self.selection[0] filenames = [self.filerevmodel.graph.filename(rev)] if not filenames: return if rev is None: qtlib.editfiles(self.repo, filenames, parent=self) else: base, _ = visdiff.snapshot(self.repo, filenames, self.repo[rev]) files = [os.path.join(base, filename) for filename in filenames] qtlib.editfiles(self.repo, files, parent=self) def onSaveFileAtRevision(self, rev): rev = self.selection[0] files = [self.filerevmodel.graph.filename(rev)] if not files or rev is None: return else: qtlib.savefiles(self.repo, files, rev, parent=self) def onShowRevisionDetails(self): rev = self.selection[0] if not self.revdetails: from tortoisehg.hgqt.revdetails import RevDetailsDialog self.revdetails = RevDetailsDialog(self._repoagent, rev=rev) else: self.revdetails.setRev(rev) self.revdetails.show() self.revdetails.raise_() @pyqtSlot(QString) def onLinkActivated(self, link): link = unicode(link) if ':' in link: scheme, param = link.split(':', 1) if scheme == 'cset': rev = self.repo[hglib.fromunicode(param)].rev() return self.goto(rev) QDesktopServices.openUrl(QUrl(link)) def onRevisionSelected(self, rev): pos = self.textView.verticalScrollBar().value() ctx = self.filerevmodel.repo.changectx(rev) self.textView.setContext(ctx) self.textView.displayFile(self.filerevmodel.graph.filename(rev), None) self.textView.verticalScrollBar().setValue(pos) self.revpanel.set_revision(rev) self.revpanel.update(repo = self.repo) # It does not make sense to select more than two revisions at a time. # Rather than enforcing a max selection size we simply let the user # know when it has selected too many revisions by using the status bar @pyqtSlot() def _checkValidSelection(self): selection = self.repoview.selectedRevisions() if len(selection) > 2: msg = _('Too many rows selected for menu') else: msg = '' self.textView.showMessage.emit(msg) def goto(self, rev): index = self.filerevmodel.indexLinkedFromRev(rev) if index is not None: self.repoview.setCurrentIndex(index) else: self._show_rev = rev def showLine(self, line): self.textView.showLine(line - 1) # fileview should do -1 instead? def setFileViewMode(self, mode): self.textView.setMode(mode) def setSearchPattern(self, text): self.textView.searchbar.setPattern(text) def setSearchCaseInsensitive(self, ignorecase): self.textView.searchbar.setCaseInsensitive(ignorecase) def reload(self): self.repoview.saveSettings() super(FileLogDialog, self).reload() class FileDiffDialog(_AbstractFileDialog): """ Qt4 dialog to display diffs between different mercurial revisions of a file. """ def __init__(self, repoagent, filename): super(FileDiffDialog, self).__init__(repoagent, filename) self._readSettings() self.menu = None def closeEvent(self, event): self._writeSettings() super(FileDiffDialog, self).closeEvent(event) def _readSettings(self): s = QSettings() s.beginGroup('filediff') try: self.restoreGeometry(s.value('geom').toByteArray()) self.splitter.restoreState(s.value('splitter').toByteArray()) finally: s.endGroup() def _writeSettings(self): s = QSettings() s.beginGroup('filediff') try: s.setValue('geom', self.saveGeometry()) s.setValue('splitter', self.splitter.saveState()) finally: s.endGroup() def setupUi(self, o): self.editToolbar = QToolBar(self) self.editToolbar.setContextMenuPolicy(Qt.PreventContextMenu) self.addToolBar(Qt.ToolBarArea(Qt.TopToolBarArea), self.editToolbar) self.actionClose = QAction(self) self.actionClose.setShortcuts(QKeySequence.Close) self.actionReload = QAction(self) self.actionReload.setShortcuts(QKeySequence.Refresh) self.editToolbar.addAction(self.actionReload) self.addAction(self.actionClose) def layouttowidget(layout): w = QWidget() w.setLayout(layout) return w self.splitter = QSplitter(Qt.Vertical) self.setCentralWidget(self.splitter) self.horizontalLayout = QHBoxLayout() cs = ('fileDiffDialogLeft', _('File Differences Log Columns')) self.tableView_revisions_left = repoview.HgRepoView(self.repo, cs[0], cs, self) self.tableView_revisions_right = repoview.HgRepoView(self.repo, 'fileDiffDialogRight', cs, self) self.horizontalLayout.addWidget(self.tableView_revisions_left) self.horizontalLayout.addWidget(self.tableView_revisions_right) self.tableView_revisions_right.setSelectionMode(QAbstractItemView.SingleSelection) self.tableView_revisions_left.setSelectionMode(QAbstractItemView.SingleSelection) self.frame = QFrame() self.splitter.addWidget(layouttowidget(self.horizontalLayout)) self.splitter.addWidget(self.frame) def setupViews(self): self.tableViews = {'left': self.tableView_revisions_left, 'right': self.tableView_revisions_right} # viewers are Scintilla editors self.viewers = {} # block are diff-block displayers self.block = {} self.diffblock = blockmatcher.BlockMatch(self.frame) lay = QHBoxLayout(self.frame) lay.setSpacing(0) lay.setContentsMargins(0, 0, 0, 0) try: contents = open(self.repo.wjoin(self.filename), "rb").read(1024) lexer = lexers.getlexer(self.repo.ui, self.filename, contents, self) except Exception: lexer = None for side, idx in (('left', 0), ('right', 3)): sci = _FileDiffScintilla(self.frame) sci.installEventFilter(self) sci.verticalScrollBar().setFocusPolicy(Qt.StrongFocus) sci.setFocusProxy(sci.verticalScrollBar()) sci.verticalScrollBar().installEventFilter(self) sci.setFrameShape(QFrame.NoFrame) sci.setMarginLineNumbers(1, True) sci.SendScintilla(sci.SCI_SETSELEOLFILLED, True) sci.setLexer(lexer) if lexer is None: sci.setFont(qtlib.getfont('fontdiff').font()) sci.setReadOnly(True) sci.setUtf8(True) lay.addWidget(sci) # hide margin 0 (markers) sci.SendScintilla(sci.SCI_SETMARGINTYPEN, 0, 0) sci.SendScintilla(sci.SCI_SETMARGINWIDTHN, 0, 0) # setup margin 1 for line numbers only sci.SendScintilla(sci.SCI_SETMARGINTYPEN, 1, 1) sci.SendScintilla(sci.SCI_SETMARGINWIDTHN, 1, 20) sci.SendScintilla(sci.SCI_SETMARGINMASKN, 1, 0) # define markers for colorize zones of diff self.markerplus = sci.markerDefine(QsciScintilla.Background) sci.setMarkerBackgroundColor(_colormap['+'], self.markerplus) self.markerminus = sci.markerDefine(QsciScintilla.Background) sci.setMarkerBackgroundColor(_colormap['-'], self.markerminus) self.markertriangle = sci.markerDefine(QsciScintilla.Background) sci.setMarkerBackgroundColor(_colormap['x'], self.markertriangle) self.markerplusline = sci.markerDefine(QsciScintilla.Invisible, _MARKERPLUSLINE) self.markerminusline = sci.markerDefine(QsciScintilla.Invisible, _MARKERMINUSLINE) self.markerplusunderline = sci.markerDefine(QsciScintilla.Invisible, _MARKERPLUSUNDERLINE) self.markerminusunderline = sci.markerDefine(QsciScintilla.Invisible, _MARKERMINUSUNDERLINE) self.viewers[side] = sci blk = blockmatcher.BlockList(self.frame) blk.linkScrollBar(sci.verticalScrollBar()) self.diffblock.linkScrollBar(sci.verticalScrollBar(), side) lay.insertWidget(idx, blk) self.block[side] = blk lay.insertWidget(2, self.diffblock) for side in sides: table = getattr(self, 'tableView_revisions_%s' % side) table.setTabKeyNavigation(False) #table.installEventFilter(self) table.revisionSelected.connect(self.onRevisionSelected) table.revisionActivated.connect(self.onRevisionActivated) l, r = (self.viewers[k].verticalScrollBar() for k in sides) l.valueChanged.connect(self.sbar_changed_left) r.valueChanged.connect(self.sbar_changed_right) l, r = (self.viewers[k].horizontalScrollBar() for k in sides) l.valueChanged.connect(r.setValue) r.valueChanged.connect(l.setValue) self.setTabOrder(table, self.viewers['left']) self.setTabOrder(self.viewers['left'], self.viewers['right']) # timer used to merge requests of syncPageStep on ResizeEvent self._delayedSyncPageStep = QTimer(self, interval=0, singleShot=True) self._delayedSyncPageStep.timeout.connect(self.diffblock.syncPageStep) # timer used to fill viewers with diff block markers during GUI idle time self.timer = QTimer() self.timer.setSingleShot(False) self.timer.timeout.connect(self.idle_fill_files) def setupModels(self): self.filedata = {'left': None, 'right': None} self._invbarchanged = False self.filerevmodel = filerevmodel.FileRevModel(self.repo, self.tableView_revisions_left.colselect[0], self.filename, parent=self) self.filerevmodel.filled.connect(self.modelFilled) self.tableView_revisions_left.setModel(self.filerevmodel) self.tableView_revisions_right.setModel(self.filerevmodel) self.tableView_revisions_left.menuRequested.connect(self.viewMenuRequest) self.tableView_revisions_right.menuRequested.connect(self.viewMenuRequest) def createActions(self): self.actionClose.triggered.connect(self.close) self.actionReload.triggered.connect(self.reload) self.actionReload.setIcon(qtlib.geticon('view-refresh')) self.actionNextDiff = QAction(qtlib.geticon('go-down'), _('Next diff'), self) self.actionNextDiff.setShortcut('Alt+Down') self.actionNextDiff.triggered.connect(self.nextDiff) self.actionPrevDiff = QAction(qtlib.geticon('go-up'), _('Previous diff'), self) self.actionPrevDiff.setShortcut('Alt+Up') self.actionPrevDiff.triggered.connect(self.prevDiff) self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) def setupToolbars(self): self.editToolbar.addSeparator() self.editToolbar.addAction(self.actionNextDiff) self.editToolbar.addAction(self.actionPrevDiff) def modelFilled(self): self.tableView_revisions_left.resizeColumns() self.tableView_revisions_right.resizeColumns() if self._show_rev is not None: self.goto(self._show_rev) self._show_rev = None elif self.tableView_revisions_left.currentIndex().isValid(): return # already set by goto() elif len(self.filerevmodel.graph): self.goto(self.filerevmodel.graph[0].rev) def eventFilter(self, watched, event): if watched in self.viewers.values(): # copy page steps to diffblock _after_ viewers are resized; resize # events will be posted in arbitrary order. if event.type() == QEvent.Resize: self._delayedSyncPageStep.start() return False else: return super(FileDiffDialog, self).eventFilter(watched, event) def onRevisionSelected(self, rev): if rev is None or rev not in self.filerevmodel.graph.nodesdict: return if self.sender() is self.tableView_revisions_right: side = 'right' else: side = 'left' path = self.filerevmodel.graph.nodesdict[rev].extra[0] fc = self.repo.changectx(rev).filectx(path) data = hglib.tounicode(fc.data()) self.filedata[side] = data.splitlines() self.update_diff(keeppos=otherside[side]) def goto(self, rev): index = self.filerevmodel.indexLinkedFromRev(rev) if index is not None: if index.row() == 0: index = self.filerevmodel.index(1, 0) self.tableView_revisions_left.setCurrentIndex(index) index = self.filerevmodel.index(0, 0) self.tableView_revisions_right.setCurrentIndex(index) else: self._show_rev = rev def setDiffNavActions(self, pos=0): hasdiff = (self.diffblock.nDiffs() > 0) self.actionNextDiff.setEnabled(hasdiff and pos != 1) self.actionPrevDiff.setEnabled(hasdiff and pos != -1) def nextDiff(self): self.setDiffNavActions(self.diffblock.nextDiff()) def prevDiff(self): self.setDiffNavActions(self.diffblock.prevDiff()) def update_page_steps(self, keeppos=None): for side in sides: self.block[side].syncPageStep() self.diffblock.syncPageStep() if keeppos: side, pos = keeppos self.viewers[side].verticalScrollBar().setValue(pos) def idle_fill_files(self): # we make a burst of diff-lines computed at once, but we # disable GUI updates for efficiency reasons, then only # refresh GUI at the end of the burst for side in sides: self.viewers[side].setUpdatesEnabled(False) self.block[side].setUpdatesEnabled(False) self.diffblock.setUpdatesEnabled(False) for n in range(30): # burst pool if self._diff is None or not self._diff.get_opcodes(): self._diff = None self.timer.stop() self.setDiffNavActions(-1) break tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0) w = self.viewers['left'] cposl = w.SendScintilla(w.SCI_GETENDSTYLED) w = self.viewers['right'] cposr = w.SendScintilla(w.SCI_GETENDSTYLED) if tag == 'replace': self.block['left'].addBlock('x', alo, ahi) self.block['right'].addBlock('x', blo, bhi) self.diffblock.addBlock('x', alo, ahi, blo, bhi) w = self.viewers['left'] for i in range(alo, ahi): w.markerAdd(i, self.markertriangle) w = self.viewers['right'] for i in range(blo, bhi): w.markerAdd(i, self.markertriangle) elif tag == 'delete': self.block['left'].addBlock('-', alo, ahi) self.diffblock.addBlock('-', alo, ahi, blo, bhi) w = self.viewers['left'] for i in range(alo, ahi): w.markerAdd(i, self.markerminus) w = self.viewers['right'] if blo < w.lines(): w.markerAdd(blo, self.markerminusline) else: w.markerAdd(blo - 1, self.markerminusunderline) elif tag == 'insert': self.block['right'].addBlock('+', blo, bhi) self.diffblock.addBlock('+', alo, ahi, blo, bhi) w = self.viewers['left'] if alo < w.lines(): w.markerAdd(alo, self.markerplusline) else: w.markerAdd(alo - 1, self.markerplusunderline) w = self.viewers['right'] for i in range(blo, bhi): w.markerAdd(i, self.markerplus) elif tag == 'equal': pass else: raise ValueError, 'unknown tag %r' % (tag,) # ok, let's enable GUI refresh for code viewers and diff-block displayers for side in sides: self.viewers[side].setUpdatesEnabled(True) self.block[side].setUpdatesEnabled(True) self.diffblock.setUpdatesEnabled(True) def update_diff(self, keeppos=None): """ Recompute the diff, display files and starts the timer responsible for filling diff markers """ if keeppos: pos = self.viewers[keeppos].verticalScrollBar().value() keeppos = (keeppos, pos) for side in sides: self.viewers[side].clear() self.block[side].clear() self.diffblock.clear() if None not in self.filedata.values(): if self.timer.isActive(): self.timer.stop() for side in sides: self.viewers[side].setMarginWidth(1, "00%s" % len(self.filedata[side])) self._diff = difflib.SequenceMatcher(None, self.filedata['left'], self.filedata['right']) blocks = self._diff.get_opcodes()[:] self._diffmatch = {'left': [x[1:3] for x in blocks], 'right': [x[3:5] for x in blocks]} for side in sides: self.viewers[side].setText(u'\n'.join(self.filedata[side])) self.update_page_steps(keeppos) self.timer.start() @pyqtSlot(int) def sbar_changed_left(self, value): self.sbar_changed(value, 'left') @pyqtSlot(int) def sbar_changed_right(self, value): self.sbar_changed(value, 'right') def sbar_changed(self, value, side): """ Callback called when a scrollbar of a file viewer is changed, so we can update the position of the other file viewer. """ if self._invbarchanged or not hasattr(self, '_diffmatch'): # prevent loops in changes (left -> right -> left ...) return self._invbarchanged = True oside = otherside[side] for i, (lo, hi) in enumerate(self._diffmatch[side]): if lo <= value < hi: break dv = value - lo blo, bhi = self._diffmatch[oside][i] vbar = self.viewers[oside].verticalScrollBar() if (dv) < (bhi - blo): bvalue = blo + dv else: bvalue = bhi vbar.setValue(bvalue) self._invbarchanged = False def reload(self): self.tableView_revisions_left.saveSettings() self.tableView_revisions_right.saveSettings() super(FileDiffDialog, self).reload() @pyqtSlot(QPoint, object) def viewMenuRequest(self, point, selection): 'User requested a context menu in repo view widget' if not selection: return if self.menu is None: self.menu = menu = QMenu(self) a = menu.addAction(_('&Diff to Parent')) a.setIcon(qtlib.geticon('visualdiff')) a.triggered.connect(self.onVisualDiff) a = menu.addAction(_('Diff to &Local')) a.setIcon(qtlib.geticon('ldiff')) a.triggered.connect(self.onVisualDiffToLocal) menu.addSeparator() a = menu.addAction(_('Diff &File to Parent')) a.setIcon(qtlib.geticon('visualdiff')) a.triggered.connect(self.onVisualDiffFile) a = menu.addAction(_('Diff File to Lo&cal')) a.setIcon(qtlib.geticon('ldiff')) a.triggered.connect(self.onVisualDiffFileToLocal) menu.addSeparator() a = menu.addAction(_('&View at Revision')) a.setIcon(qtlib.geticon('view-at-revision')) a.triggered.connect(self.onViewFileAtRevision) a = menu.addAction(_('&Save at Revision...')) a.triggered.connect(self.onSaveFileAtRevision) a = menu.addAction(_('&Edit Local')) a.setIcon(qtlib.geticon('edit-file')) a.triggered.connect(self.onEditLocal) a = menu.addAction(_('&Revert to Revision...')) a.setIcon(qtlib.geticon('hg-revert')) a.triggered.connect(self.onRevertFileToRevision) self.selection = selection self.menu.exec_(point) def onVisualDiff(self): opts = dict(change=self.selection[0]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def onVisualDiffToLocal(self): opts = dict(rev=['rev(%d)' % self.selection[0]]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def onVisualDiffFile(self): rev = self.selection[0] paths = [self.filerevmodel.graph.filename(rev)] opts = dict(change=self.selection[0]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def onVisualDiffFileToLocal(self): rev = self.selection[0] paths = [self.filerevmodel.graph.filename(rev)] opts = dict(rev=['rev(%d)' % rev]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, paths, opts) if dlg: dlg.exec_() def onEditLocal(self): filenames = [self.filename] if not filenames: return qtlib.editfiles(self.repo, filenames, parent=self) def onRevertFileToRevision(self): rev = self.selection[0] if rev is None: rev = self.repo['.'].rev() fileSelection = [self.filerevmodel.graph.filename(rev)] if len(fileSelection) == 0: return dlg = revert.RevertDialog(self._repoagent, fileSelection, rev, self) if dlg: dlg.exec_() dlg.deleteLater() def onViewFileAtRevision(self): rev = self.selection[0] filenames = [self.filerevmodel.graph.filename(rev)] if not filenames: return if rev is None: qtlib.editfiles(self.repo, filenames, parent=self) else: base, _ = visdiff.snapshot(self.repo, filenames, self.repo[rev]) files = [os.path.join(base, filename) for filename in filenames] qtlib.editfiles(self.repo, files, parent=self) def onSaveFileAtRevision(self, rev): rev = self.selection[0] files = [self.filerevmodel.graph.filename(rev)] if not files or rev is None: return else: qtlib.savefiles(self.repo, files, rev, parent=self) tortoisehg-2.10/tortoisehg/hgqt/hgrcutil.py0000644000076400007640000000327312110205646020220 0ustar stevesteve# hgrcutils.py - Functions to manipulate hgrc (or similar) files # # Copyright 2011 Angel Ezquerra # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.util import wconfig def loadIniFile(rcpath, parent=None): for fn in rcpath: if os.path.exists(fn): break else: for fn in rcpath: # Try to create a file from rcpath try: f = open(fn, 'w') f.write('# Generated by TortoiseHg\n') f.close() break except EnvironmentError: pass else: qtlib.WarningMsgBox(_('Unable to create a config file'), _('Insufficient access rights.'), parent=parent) return None, {} return fn, wconfig.readfile(fn) def setConfigValue(rcfilepath, cfgpath, value): ''' Set a value on a config file, such as an hgrc or a .ini file rcpfilepath: Absolute path to a configuration file cfgpath: Full "path" of a configurable key Format is section.keyNamee.g. 'web.name') value: String value for the selected config key ''' fn, cfg = loadIniFile([rcfilepath]) if not hasattr(cfg, 'write'): return False if fn is None: return False cfgFullKey = cfgpath.split('.') if len(cfgFullKey) < 2: return False cfg.set(cfgFullKey[0], cfgFullKey[1], value) try: wconfig.writefile(cfg, fn) except EnvironmentError, e: return False return True tortoisehg-2.10/tortoisehg/hgqt/graph.py0000644000076400007640000003647412235634453017523 0ustar stevesteve# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. """helper functions and classes to ease hg revision graph building Based on graphlog's algorithm, with inspiration stolen from TortoiseHg revision grapher (now stolen back). The primary interface are the *_grapher functions, which are generators of Graph instances that describe a revision set graph. These generators are used by repomodel.py which renders them on a widget. """ import time import os import itertools from mercurial import repoview LINE_TYPE_PARENT = 0 LINE_TYPE_GRAFT = 1 def revision_grapher(repo, **opts): """incremental revision grapher param repo The repository opt start_rev Tip-most revision of range to graph opt stop_rev 0-most revision of range to graph opt follow True means graph only ancestors of start_rev opt revset set of revisions to graph. If used, then start_rev, stop_rev, and follow is ignored opt branch Only graph this branch opt allparents If set in addition to branch, then cset outside the branch that are ancestors to some cset inside the branch is also graphed This generator function walks through the revision history from revision start_rev to revision stop_rev (which must be less than or equal to start_rev) and for each revision emits tuples with the following elements: - current revision - column of the current node in the set of ongoing edges - color of the node (?) - lines: a list of (col, next_col, color_no, line_type, children, parent) children: tuple of revs which connected to top of this line. (or current rev if node is on the line.) parent: rev which connected to bottom of this line. defining the edges between the current row and the next row - parent revisions of current revision """ revset = opts.get('revset', None) branch = opts.get('branch', None) showhidden = opts.get('showhidden', None) showgraftsource = opts.get('showgraftsource', None) if showhidden: revhidden = [] else: revhidden = repoview.filterrevs(repo, 'visible') if revset: start_rev = max(revset) stop_rev = min(revset) follow = False hidden = lambda rev: (rev not in revset) or (rev in revhidden) else: start_rev = opts.get('start_rev', None) stop_rev = opts.get('stop_rev', 0) follow = opts.get('follow', False) hidden = lambda rev: rev in revhidden assert start_rev is None or start_rev >= stop_rev curr_rev = start_rev revs = [] children = [()] links = [] # smallest link type that applies if opts.get('allparents') or not branch: def getparents(ctx): return [x for x in ctx.parents() if x] else: def getparents(ctx): return [x for x in ctx.parents() if x and x.branch() == branch] rev_color = RevColorPalette(getparents) while curr_rev is None or curr_rev >= stop_rev: if hidden(curr_rev): curr_rev -= 1 continue # Compute revs and next_revs. ctx = repo[curr_rev] if curr_rev not in revs: if branch and ctx.branch() != branch: if curr_rev is None: curr_rev = len(repo) else: curr_rev -= 1 yield None continue # New head. if start_rev and follow and curr_rev != start_rev: curr_rev -= 1 continue revs.append(curr_rev) links.append(LINE_TYPE_PARENT) children.append(()) rev_color.addheadctx(ctx) curcolor = rev_color[curr_rev] rev_index = revs.index(curr_rev) next_revs = revs[:] next_links = links[:] next_children = children[:] # Add parents to next_revs. parents = [(p.rev(), LINE_TYPE_PARENT) for p in getparents(ctx) if not hidden(p.rev())] if showgraftsource and 'source' in ctx.extra(): src_rev_str = ctx.extra()['source'] if src_rev_str in repo: src_rev = repo[src_rev_str].rev() if stop_rev <= src_rev < curr_rev and not hidden(src_rev): parents.append((src_rev, LINE_TYPE_GRAFT)) parents_to_add = [] links_to_add = [] children_to_add = [] if len(parents) > 1: preferred_color = None else: preferred_color = curcolor for parent, link_type in parents: if parent not in next_revs: parents_to_add.append(parent) links_to_add.append(link_type) children_to_add.append((curr_rev,)) if parent not in rev_color: rev_color.assigncolor(parent, preferred_color) preferred_color = None else: # Merging lines should have the most solid style # (= lowest style value) i = next_revs.index(parent) next_links[i] = min(next_links[i], link_type) next_children[i] += (curr_rev,) preferred_color = None # parents_to_add.sort() next_revs[rev_index:rev_index + 1] = parents_to_add next_links[rev_index:rev_index + 1] = links_to_add next_children[rev_index:rev_index + 1] = children_to_add lines = [] for i, rev in enumerate(revs): if rev in next_revs: color = rev_color[rev] lines.append((i, next_revs.index(rev), color, links[i], children[i], rev)) elif rev == curr_rev: for parent, link_type in parents: color = rev_color[parent] lines.append((i, next_revs.index(parent), color, link_type, (curr_rev,), parent)) yield GraphNode(curr_rev, rev_index, curcolor, lines, parents) revs = next_revs links = next_links children = next_children if curr_rev is None: curr_rev = len(repo) else: curr_rev -= 1 def filelog_grapher(repo, path): ''' Graph the ancestry of a single file (log). Deletions show up as breaks in the graph. ''' filerev = len(repo.file(path)) - 1 fctx = repo.filectx(path, fileid=filerev) rev = fctx.rev() flog = fctx.filelog() heads = [repo.filectx(path, fileid=flog.rev(x)).rev() for x in flog.heads()] assert rev in heads heads.remove(rev) revs = [] children = [()] rev_color = {} nextcolor = 0 _paths = {} while rev >= 0: # Compute revs and next_revs if rev not in revs: revs.append(rev) rev_color[rev] = nextcolor ; nextcolor += 1 children.append(()) curcolor = rev_color[rev] index = revs.index(rev) next_revs = revs[:] next_children = children[:] # Add parents to next_revs fctx = repo.filectx(_paths.get(rev, path), changeid=rev) for pfctx in fctx.parents(): _paths[pfctx.rev()] = pfctx.path() parents = [pfctx.rev() for pfctx in fctx.parents()] # if f.path() == path] parents_to_add = [] children_to_add = [] for parent in parents: if parent not in next_revs: parents_to_add.append(parent) children_to_add.append((rev,)) if len(parents) > 1: rev_color[parent] = nextcolor ; nextcolor += 1 else: rev_color[parent] = curcolor parents_to_add.sort() next_revs[index:index + 1] = parents_to_add next_children[index:index + 1] = children_to_add lines = [] for i, nrev in enumerate(revs): if nrev in next_revs: color = rev_color[nrev] lines.append((i, next_revs.index(nrev), color, LINE_TYPE_PARENT, children[i], nrev)) elif nrev == rev: for parent in parents: color = rev_color[parent] lines.append((i, next_revs.index(parent), color, LINE_TYPE_PARENT, (rev,), parent)) pcrevs = [pfc.rev() for pfc in fctx.parents()] yield GraphNode(fctx.rev(), index, curcolor, lines, pcrevs, extra=[_paths.get(fctx.rev(), path)]) revs = next_revs children = next_children if revs: rev = max(revs) else: rev = -1 if heads and rev <= heads[-1]: rev = heads.pop() def mq_patch_grapher(repo): """Graphs unapplied MQ patches""" for patchname in reversed(repo.thgmqunappliedpatches): yield GraphNode(patchname, 0, "", [], []) class RevColorPalette(object): """Assign node and line colors for each revision""" def __init__(self, getparents): self._getparents = getparents self._pendingheads = [] self._knowncolors = {} self._nextcolor = 0 def addheadctx(self, ctx): color = self.assigncolor(ctx.rev()) p_ctxs = self._getparents(ctx) self._pendingheads.append((p_ctxs, color)) def _fillpendingheads(self, stoprev): if stoprev is None: return # avoid filling everything (int_rev < None is False) nextpendingheads = [] for p_ctxs, color in self._pendingheads: pending = self._fillancestors(p_ctxs, color, stoprev) if pending: nextpendingheads.append((pending, color)) self._pendingheads = nextpendingheads def _fillancestors(self, p_ctxs, curcolor, stoprev): while p_ctxs: ctx0 = p_ctxs[0] rev0 = ctx0.rev() if rev0 < stoprev: return p_ctxs if rev0 in self._knowncolors: return self._knowncolors[rev0] = curcolor p_ctxs = self._getparents(ctx0) def assigncolor(self, rev, color=None): self._fillpendingheads(rev) if color is None: color = self._nextcolor self._nextcolor += 1 self._knowncolors[rev] = color return color def __getitem__(self, rev): self._fillpendingheads(rev) return self._knowncolors[rev] def __contains__(self, rev): self._fillpendingheads(rev) return rev in self._knowncolors class GraphNode(object): """ Simple class to encapsulate a hg node in the revision graph. Does nothing but declaring attributes. """ def __init__(self, rev, xposition, color, lines, parents, ncols=None, extra=None): self.rev = rev self.x = xposition self.color = color if ncols is None: ncols = len(lines) self.cols = ncols self.parents = parents self.bottomlines = lines self.toplines = [] self.extra = extra class Graph(object): """ Graph object to ease hg repo navigation. The Graph object instantiate a `revision_grapher` generator, and provide a `fill` method to build the graph progressively. """ #@timeit def __init__(self, repo, grapher, include_mq=False): self.repo = repo self.maxlog = len(repo) if include_mq: patch_grapher = mq_patch_grapher(self.repo) self.grapher = itertools.chain(patch_grapher, grapher) else: self.grapher = grapher self.nodes = [] self.nodesdict = {} self.max_cols = 0 def __getitem__(self, idx): if isinstance(idx, slice): # XXX TODO: ensure nodes are built return self.nodes.__getitem__(idx) if idx >= len(self.nodes): # build as many graph nodes as required to answer the # requested idx self.build_nodes(idx) if idx >= len(self): return self.nodes[-1] return self.nodes[idx] def __len__(self): # len(graph) is the number of actually built graph nodes return len(self.nodes) def build_nodes(self, nnodes=None, rev=None): """ Build up to `nnodes` more nodes in our graph, or build as many nodes required to reach `rev`. If both rev and nnodes are set, build as many nodes as required to reach rev plus nnodes more. """ if self.grapher is None: return False usetimer = nnodes is None and rev is None if usetimer: if os.name == "nt": timer = time.clock else: timer = time.time startsec = timer() stopped = False mcol = set([self.max_cols]) for gnode in self.grapher: if gnode is None: continue if not type(gnode.rev) == str and gnode.rev >= self.maxlog: continue if self.nodes: gnode.toplines = self.nodes[-1].bottomlines self.nodes.append(gnode) self.nodesdict[gnode.rev] = gnode mcol = mcol.union(set([gnode.x])) mcol = mcol.union(set([max(x[:2]) for x in gnode.bottomlines])) if (rev is not None and isinstance(gnode.rev, int) and gnode.rev <= rev): rev = None # we reached rev, switching to nnode counter if rev is None: if nnodes is not None: nnodes -= 1 if not nnodes: break if usetimer: cursec = timer() if cursec < startsec or cursec > startsec + 0.1: break else: self.grapher = None stopped = True self.max_cols = max(mcol) + 1 return not stopped def isfilled(self): return self.grapher is None def index(self, rev): if len(self) == 0: # graph is empty, let's build some nodes. nodes for unapplied # patches are built at once because they don't have comparable # revision numbers, which makes build_nodes() go wrong. self.build_nodes(10, len(self.repo) - 1) if isinstance(rev, int) and len(self) > 0 and rev < self.nodes[-1].rev: self.build_nodes(self.nodes[-1].rev - rev) if rev in self.nodesdict: return self.nodes.index(self.nodesdict[rev]) return -1 # # File graph method # def filename(self, rev): return self.nodesdict[rev].extra[0] tortoisehg-2.10/tortoisehg/hgqt/branchop.py0000644000076400007640000001032212145761533020176 0ustar stevesteve# branchop.py - branch operations dialog for TortoiseHg commit tool # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.hgqt.i18n import _ from tortoisehg.util import hglib from tortoisehg.hgqt import qtlib class BranchOpDialog(QDialog): 'Dialog for manipulating wctx.branch()' def __init__(self, repo, oldbranchop, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - branch operation') % repo.displayname) self.setWindowIcon(qtlib.geticon('branch')) layout = QVBoxLayout() self.setLayout(layout) wctx = repo[None] if len(wctx.parents()) == 2: lbl = QLabel(''+_('Select branch of merge commit')+'') layout.addWidget(lbl) branchCombo = QComboBox() # If both parents belong to the same branch, do not duplicate the # branch name in the branch select combo branchlist = [p.branch() for p in wctx.parents()] if branchlist[0] == branchlist[1]: branchlist = [branchlist[0]] for b in branchlist: branchCombo.addItem(hglib.tounicode(b)) layout.addWidget(branchCombo) else: text = ''+_('Changes take effect on next commit')+'' lbl = QLabel(text) layout.addWidget(lbl) grid = QGridLayout() nochange = QRadioButton(_('No branch changes')) newbranch = QRadioButton(_('Open a new named branch')) closebranch = QRadioButton(_('Close current branch')) branchCombo = QComboBox() branchCombo.setEditable(True) qtlib.allowCaseChangingInput(branchCombo) wbu = wctx.branch() for name in repo.namedbranches: if name == wbu: continue branchCombo.addItem(hglib.tounicode(name)) branchCombo.activated.connect(self.accept) grid.addWidget(nochange, 0, 0) grid.addWidget(newbranch, 1, 0) grid.addWidget(branchCombo, 1, 1) grid.addWidget(closebranch, 2, 0) grid.setColumnStretch(0, 0) grid.setColumnStretch(1, 1) layout.addLayout(grid) layout.addStretch() newbranch.toggled.connect(branchCombo.setEnabled) branchCombo.setEnabled(False) if oldbranchop is None: nochange.setChecked(True) elif oldbranchop == False: closebranch.setChecked(True) else: assert type(oldbranchop) == QString bc = branchCombo names = [bc.itemText(i) for i in xrange(bc.count())] if oldbranchop in names: bc.setCurrentIndex(names.index(oldbranchop)) else: bc.addItem(oldbranchop) bc.setCurrentIndex(len(names)) newbranch.setChecked(True) self.closebranch = closebranch BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setAutoDefault(True) layout.addWidget(bb) self.bb = bb self.branchCombo = branchCombo QShortcut(QKeySequence('Ctrl+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) QShortcut(QKeySequence('Escape'), self, self.reject) def accept(self): '''Branch operation is one of: None - leave wctx branch name untouched False - close current branch QString - open new named branch ''' if self.branchCombo.isEnabled(): # branch name cannot start/end with whitespace (see dirstate._branch) self.branchop = self.branchCombo.currentText().trimmed() elif self.closebranch.isChecked(): self.branchop = False else: self.branchop = None QDialog.accept(self) tortoisehg-2.10/tortoisehg/hgqt/pbranch.py0000644000076400007640000007402012231647662020026 0ustar stevesteve# pbranch.py - TortoiseHg's patch branch widget # # Copyright 2010 Peer Sommerlund # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import errno from mercurial import extensions, error, util from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, cmdcore, cmdui, update, revdetails from tortoisehg.hgqt.qtlib import geticon from tortoisehg.util import hglib from PyQt4.QtCore import * from PyQt4.QtGui import * PATCHCACHEPATH = 'thgpbcache' nullvariant = QVariant() class PatchBranchWidget(QWidget, qtlib.TaskWidget): ''' A widget that show the patch graph and provide actions for the pbranch extension ''' def __init__(self, repoagent, parent=None, logwidget=None): QWidget.__init__(self, parent) # Set up variables and connect signals self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self.pbranch = extensions.find('pbranch') # Unfortunately global instead of repo-specific self.show_internal_branches = False repoagent.configChanged.connect(self.configChanged) repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.workingBranchChanged.connect(self.workingBranchChanged) # Build child widgets def BuildChildWidgets(): vbox = QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) self.setLayout(vbox) vbox.addWidget(Toolbar(), 1) vbox.addWidget(BelowToolbar(), 1) def Toolbar(): tb = QToolBar(_("Patch Branch Toolbar"), self) tb.setEnabled(True) tb.setObjectName("toolBar_patchbranch") tb.setFloatable(False) self.actionPMerge = a = QWidgetAction(self) a.setIcon(geticon("hg-merge")) a.setToolTip(_('Merge all pending dependencies')) tb.addAction(self.actionPMerge) self.actionPMerge.triggered.connect(self.pmerge_clicked) self.actionBackport = a = QWidgetAction(self) a.setIcon(geticon("go-previous")) a.setToolTip(_('Backout current patch branch')) #tb.addAction(self.actionBackport) #self.actionBackport.triggered.connect(self.pbackout_clicked) self.actionReapply = a = QWidgetAction(self) a.setIcon(geticon("go-next")) a.setToolTip(_('Backport part of a changeset to a dependency')) #tb.addAction(self.actionReapply) #self.actionReapply.triggered.connect(self.reapply_clicked) self.actionPNew = a = QWidgetAction(self) a.setIcon(geticon("fileadd")) #STOCK_NEW a.setToolTip(_('Start a new patch branch')) tb.addAction(self.actionPNew) self.actionPNew.triggered.connect(self.pnew_clicked) self.actionEditPGraph = a = QWidgetAction(self) a.setIcon(geticon("edit-file")) #STOCK_EDIT a.setToolTip(_('Edit patch dependency graph')) tb.addAction(self.actionEditPGraph) self.actionEditPGraph.triggered.connect(self.edit_pgraph_clicked) return tb def BelowToolbar(): w = QSplitter(self) w.addWidget(PatchList()) w.addWidget(PatchDiff()) return w def PatchList(): self.patchlistmodel = PatchBranchModel(self.compute_model(), self.repo.changectx('.').branch(), self) self.patchlist = QTableView(self) self.patchlist.setModel(self.patchlistmodel) self.patchlist.setShowGrid(False) self.patchlist.verticalHeader().setDefaultSectionSize(20) self.patchlist.horizontalHeader().setHighlightSections(False) self.patchlist.setSelectionBehavior(QAbstractItemView.SelectRows) self.patchlist.clicked.connect(self.patchBranchSelected) return self.patchlist def PatchDiff(): # pdiff view to the right of pgraph self.patchDiffStack = QStackedWidget() self.patchDiffStack.addWidget(PatchDiffMessage()) self.patchDiffStack.addWidget(PatchDiffDetails()) return self.patchDiffStack def PatchDiffMessage(): # message if no patch is selected self.patchDiffMessage = QLabel() self.patchDiffMessage.setAlignment(Qt.AlignCenter) return self.patchDiffMessage def PatchDiffDetails(): # pdiff view of selected patc self.patchdiff = revdetails.RevDetailsWidget(self._repoagent, self) return self.patchdiff BuildChildWidgets() @property def repo(self): return self._repoagent.rawRepo() def reload(self): 'User has requested a reload' self.repo.thginvalidate() self.refresh() def refresh(self): """ Refresh the list of patches. This operation will try to keep selection state. """ if not self.pbranch: return # store selected patch name selname = None patchnamecol = PatchBranchModel._columns.index('Name') # Column used to store patch name selinxs = self.patchlist.selectedIndexes() if len(selinxs) > 0: selrow = selinxs[0].row() patchnameinx = self.patchlist.model().index(selrow, patchnamecol) selname = self.patchlist.model().data(patchnameinx) # compute model data self.patchlistmodel.setModel( self.compute_model(), self.repo.changectx('.').branch() ) # restore patch selection if selname: selinxs = self.patchlistmodel.match( self.patchlistmodel.index(0, patchnamecol), Qt.DisplayRole, selname, flags = Qt.MatchExactly) if len(selinxs) > 0: self.patchlist.setCurrentIndex(selinxs[0]) # update UI sensitives self.update_sensitivity() # # Data functions # def compute_model(self): """ Compute content of table, including patch graph and other columns """ # compute model data model = [] # Generate patch branch graph from all heads (option --tips) opts = {'tips': True} mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) graph = mgr.graphforopts(opts) target_graph = mgr.graphforopts({}) if not self.show_internal_branches: graph = mgr.patchonlygraph(graph) names = None patch_list = graph.topolist(names) in_lines = [] if patch_list: dep_list = [patch_list[0]] cur_branch = self.repo['.'].branch() patch_status = {} for name in patch_list: patch_status[name] = self.pstatus(name) for name in patch_list: parents = graph.deps(name) # Node properties if name in dep_list: node_column = dep_list.index(name) else: node_column = len(dep_list) node_color = patch_status[name] and '#ff0000' or 0 node_status = nodestatus_NORMAL if graph.ispatch(name) and not target_graph.ispatch(name): node_status = nodestatus_CLOSED if name == cur_branch: node_status = node_status | nodestatus_CURRENT node = PatchGraphNodeAttributes(node_column, node_color, node_status) # Find next dependency list my_deps = [] for p in parents: if p not in dep_list: my_deps.append(p) next_dep_list = dep_list[:] next_dep_list[node_column:node_column+1] = my_deps # Dependency lines shift = len(parents) - 1 out_lines = [] for p in parents: dep_column = next_dep_list.index(p) color = 0 # black if patch_status[p]: color = '#ff0000' # red style = 0 # solid lines out_lines.append(GraphLine(node_column, dep_column, color, style)) for line in in_lines: if line.end_column == node_column: # Deps to current patch end here pass else: # Find line continuations dep = dep_list[line.end_column] dep_column = next_dep_list.index(dep) out_lines.append(GraphLine(line.end_column, dep_column, line.color, line.style)) stat = patch_status[name] and 'M' or 'C' # patch status patchname = name msg = self.pmessage(name) # summary if msg: title = msg.split('\n')[0] else: title = None model.append(PatchGraphNode(node, in_lines, out_lines, patchname, stat, title, msg)) # Loop in_lines = out_lines dep_list = next_dep_list return model # # pbranch extension functions # def pgraph(self): """ [pbranch] Execute 'pgraph' command. :returns: A list of patches and dependencies """ if self.pbranch is None: return None opts = {} mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) return mgr.graphforopts(opts) def pstatus(self, patch_name): """ [pbranch] Execute 'pstatus' command. :param patch_name: Name of patch-branch :retv: list of status messages. If empty there is no pending merges """ if self.pbranch is None: return None status = [] opts = {} mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) graph = mgr.graphforopts(opts) graph_cur = mgr.graphforopts({'tips': True}) heads = self.repo.branchheads(patch_name) if graph_cur.isinner(patch_name) and not graph.isinner(patch_name): status.append(_('will be closed')) if len(heads) > 1: status.append(_('needs merge of %i heads\n') % len(heads)) for dep, through in graph.pendingmerges(patch_name): if through: status.append(_('needs merge with %s (through %s)\n') % (dep, ", ".join(through))) else: status.append(_('needs merge with %s\n') % dep) for dep in graph.pendingrebases(patch_name): status.append(_('needs update of diff base to tip of %s\n') % dep) return status def pmessage(self, patch_name): """ Get patch message :param patch_name: Name of patch-branch :retv: Full patch message. If you extract the first line you will get the patch title. If the repo does not contain message or patch, the function returns None """ opts = {} mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) try: return mgr.patchdesc(patch_name) except: return None def pdiff(self, patch_name): """ [pbranch] Execute 'pdiff --tips' command. :param patch_name: Name of patch-branch :retv: list of lines of generated patch """ opts = {} mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) graph = mgr.graphattips() return graph.diff(patch_name, None, opts) def pnew_ui(self): """ Create new patch. Prompt user for new patch name. Patch is created on current branch. """ dialog = PNewDialog() if dialog.exec_() != QDialog.Accepted: return False cmdline = dialog.getCmd() self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self.commandFinished) return True def pnew(self, patch_name): """ [pbranch] Execute 'pnew' command. :param patch_name: Name of new patch-branch """ if self.pbranch is None: return False self.repo.incrementBusyCount() self.pbranch.cmdnew(self.repo.ui, self.repo, patch_name) self.repo.decrementBusyCount() return True def pmerge(self, patch_name=None): """ [pbranch] Execute 'pmerge' command. :param patch_name: Merge to this patch-branch """ if not self.has_patch(): return cmdline = ['pmerge'] if patch_name: cmdline += [hglib.tounicode(patch_name)] else: cmdline += ['--all'] self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self.commandFinished) def has_pbranch(self): """ return True if pbranch extension can be used """ return self.pbranch is not None def has_patch(self): """ return True if pbranch extension is in use on repo """ return self.has_pbranch() and self.pgraph() != [] def is_patch(self, branch_name): """ return True if branch is a patch. This excludes root branches and internal diff base branches (for patches with multiple dependencies). """ return self.has_pbranch() and self.pgraph().ispatch(branch_name) def cur_branch(self): """ Return branch that workdir belongs to. """ return self.repo.dirstate.branch() ### internal functions ### def patchFromIndex(self, index): if not index.isValid(): return model = self.patchlistmodel col = model._columns.index('Name') patchIndex = model.createIndex(index.row(), col) return str(model.data(patchIndex).toString()) def updatePatchCache(self, patchname): # TODO: Parameters should include rev, as one patch may have several heads # rev should be appended to filename and used by pdiff assert(len(patchname)>0) cachepath = self.repo.join(PATCHCACHEPATH) # TODO: Fix this - it looks ugly try: os.mkdir(cachepath) except OSError, err: if err.errno != errno.EEXIST: raise # TODO: Convert filename if any funny characters are present patchfile = os.path.join(cachepath, patchname) dirstate = self.repo.join('dirstate') try: patch_age = os.path.getmtime(patchfile) - os.path.getmtime(dirstate) except: patch_age = -1 if patch_age < 0: pf = open(patchfile, 'wb') try: pf.writelines(self.pdiff(patchname)) # except (util.Abort, error.RepoError), e: # # Do something with str(e) finally: pf.close() return patchfile def update_sensitivity(self): """ Update the sensitivity of entire UI """ in_pbranch = True #TODO is_merge = len(self.repo.parents()) > 1 self.actionPMerge.setEnabled(in_pbranch) self.actionBackport.setEnabled(in_pbranch) self.actionReapply.setEnabled(True) self.actionPNew.setEnabled(not is_merge) self.actionEditPGraph.setEnabled(True) def selected_patch(self): C_NAME = PatchBranchModel._columns.index('Name') indexes = self.patchlist.selectedIndexes() if len(indexes) == 0: return None index = indexes[0] return str(index.sibling(index.row(), C_NAME).data().toString()) def show_patch_cmenu(self, pos): """Context menu for selected patch""" patchname = self.selected_patch() if not patchname: return menu = QMenu(self) def append(label, handler): menu.addAction(label).triggered.connect(handler) has_pbranch = self.has_pbranch() is_current = self.has_patch() and self.cur_branch() == patchname is_patch = self.is_patch(patchname) is_internal = self.pbranch.isinternal(patchname) is_merge = len(self.repo.branchheads(patchname)) > 1 #if has_pbranch and not is_merge and not is_internal: # append(_('&New'), self.pnew_activated) if not is_current: append(_('&Goto (update workdir)'), self.goto_activated) if is_patch: append(_('&Merge'), self.merge_activated) # append(_('&Edit message'), self.edit_message_activated) # append(_('&Rename'), self.rename_activated) # append(_('&Delete'), self.delete_activated) # append(_('&Finish'), self.finish_activated) if len(menu.actions()) > 0: menu.exec_(pos) # Signal handlers def patchBranchSelected(self, index): patchname = self.patchFromIndex(index) if self.is_patch(patchname): patchfile = self.updatePatchCache(patchname) self.patchdiff.onRevisionSelected(patchfile) self.patchDiffStack.setCurrentWidget(self.patchdiff) else: self.patchDiffMessage.setText(_('No patch branch selected')) self.patchDiffStack.setCurrentWidget(self.patchDiffMessage) def contextMenuEvent(self, event): if self.patchlist.geometry().contains(event.pos()): self.show_patch_cmenu(event.globalPos()) @pyqtSlot(int) def commandFinished(self, ret): if ret != 0: cmdui.errorMessageBox(self._cmdsession, self) self.refresh() @pyqtSlot() def configChanged(self): pass @pyqtSlot() def repositoryChanged(self): self.refresh() @pyqtSlot() def workingBranchChanged(self): self.refresh() def pmerge_clicked(self): self.pmerge() def pnew_clicked(self, toolbutton): self.pnew_ui() def edit_pgraph_clicked(self): opts = {} # TODO: How to find user ID mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) if not mgr.hasgraphdesc(): self.pbranch.writefile(mgr.graphdescpath(), '') oldtext = mgr.graphdesc() # run editor in the repository root olddir = os.getcwd() os.chdir(self.repo.root) try: newtext = None newtext = self.repo.ui.edit(oldtext, opts.get('user')) except error.Abort: no_editor_configured =(os.environ.get("HGEDITOR") or self.repo.ui.config("ui", "editor") or os.environ.get("VISUAL") or os.environ.get("EDITOR","editor-not-configured") == "editor-not-configured") if no_editor_configured: qtlib.ErrorMsgBox(_('No editor found'), _('Mercurial was unable to find an editor. Please configure Mercurial to use an editor installed on your system.')) else: raise os.chdir(olddir) if newtext is not None: mgr.updategraphdesc(newtext) self.refresh() ### context menu signal handlers ### def pnew_activated(self): """Insert new patch after this row""" assert False def edit_message_activated(self): assert False def goto_activated(self): branch = self.selected_patch() # TODO: Fetch list of heads of branch # - use a list of revs if more than one found dlg = update.UpdateDialog(self._repoagent, branch, self) dlg.exec_() def merge_activated(self): self.pmerge(self.selected_patch()) def delete_activated(self): assert False def rename_activated(self): assert False def finish_activated(self): assert False class PatchGraphNode(object): """ Simple class to encapsulate a node in the patch branch graph. Does nothing but declaring attributes. """ def __init__(self, node, in_lines, out_lines, patchname, stat, title, msg): """ :node: attributes related to the node :in_lines: List of lines above node :out_lines: List of lines below node :patchname: Patch branch name :stat: Status of node - does it need updating or not :title: First line of patch message :msg: Full patch message """ self.node = node self.toplines = in_lines self.bottomlines = out_lines # Find rightmost column used self.cols = max([max(line.start_column,line.end_column) for line in in_lines + out_lines]) self.patchname = patchname self.status = stat self.title = title self.message = msg self.msg_esc = msg # u''.join(msg) # escaped summary (utf-8) nodestatus_CURRENT = 4 nodestatus_NORMAL = 0 nodestatus_PATCH = 1 nodestatus_CLOSED = 2 nodestatus_shapemask = 3 class PatchGraphNodeAttributes(object): """ Simple class to encapsulate attributes about a node in the patch branch graph. Does nothing but declaring attributes. """ def __init__(self, column, color, status): self.column = column self.color = color self.status = status class GraphLine(object): """ Simple class to encapsulate attributes about a line in the patch branch graph. Does nothing but declaring attributes. """ def __init__(self, start_column, end_column, color, style): self.start_column = start_column self.end_column = end_column self.color = color self.style = style class PatchBranchContext(object): """ Similar to patchctx in thgrepo, this class simulates a changeset for a particular patch branch- """ class PatchBranchModel(QAbstractTableModel): """ Model used to list patch branches TODO: Should be extended to list all branches """ _columns = ['Graph', 'Name', 'Status', 'Title', 'Message',] _headers = (_('Graph'), _('Name'), _('Status'), _('Title'), _('Message')) def __init__(self, model, wd_branch="", parent=None): QAbstractTableModel.__init__(self, parent) self.rowcount = 0 self._columnmap = {'Graph': lambda ctx, gnode: "", 'Name': lambda ctx, gnode: gnode.patchname, 'Status': lambda ctx, gnode: gnode.status, 'Title': lambda ctx, gnode: gnode.title, 'Message': lambda ctx, gnode: gnode.message } self.model = model self.wd_branch = wd_branch self.dotradius = 8 self.rowheight = 20 # virtual functions required to subclass QAbstractTableModel def rowCount(self, parent=None): return len(self.model) def columnCount(self, parent=None): return len(self._columns) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return nullvariant row = index.row() column = self._columns[index.column()] gnode = self.model[row] ctx = None #ctx = self.repo.changectx(gnode.rev) if role == Qt.DisplayRole: text = self._columnmap[column](ctx, gnode) if not isinstance(text, (QString, unicode)): text = hglib.tounicode(text) return QVariant(text) elif role == Qt.ForegroundRole: return gnode.node.color elif role == Qt.DecorationRole: if column == 'Graph': return self.graphctx(ctx, gnode) return nullvariant def headerData(self, section, orientation, role): if orientation == Qt.Horizontal: if role == Qt.DisplayRole: return QVariant(self._headers[section]) if role == Qt.TextAlignmentRole: return QVariant(Qt.AlignLeft) return nullvariant # end of functions required to subclass QAbstractTableModel def setModel(self, model, wd_branch): self.beginResetModel() self.model = model self.wd_branch = wd_branch self.endResetModel() def col2x(self, col): return 2 * self.dotradius * col + self.dotradius/2 + 8 def graphctx(self, ctx, gnode): """ Return a QPixmap for the patch graph for the current row :ctx: Data for current row = branch (not used) :gnode: PatchGraphNode in patch branch graph :returns: QPixmap of pgraph for ctx """ w = self.col2x(gnode.cols) + 10 h = self.rowheight dot_y = h / 2 # Prepare painting: Target pixmap, blue and black pen pix = QPixmap(w, h) pix.fill(QColor(0,0,0,0)) painter = QPainter(pix) painter.setRenderHint(QPainter.Antialiasing) pen = QPen(Qt.blue) pen.setWidth(2) painter.setPen(pen) lpen = QPen(pen) lpen.setColor(Qt.black) painter.setPen(lpen) # Draw lines for y1, y4, lines in ((dot_y, dot_y + h, gnode.bottomlines), (dot_y - h, dot_y, gnode.toplines)): y2 = y1 + 1 * (y4 - y1)/4 ymid = (y1 + y4)/2 y3 = y1 + 3 * (y4 - y1)/4 for line in lines: start = line.start_column end = line.end_column color = line.color lpen = QPen(pen) lpen.setColor(QColor(color)) lpen.setWidth(2) painter.setPen(lpen) x1 = self.col2x(start) x2 = self.col2x(end) path = QPainterPath() path.moveTo(x1, y1) path.cubicTo(x1, y2, x1, y2, (x1 + x2)/2, ymid) path.cubicTo(x2, y3, x2, y3, x2, y4) painter.drawPath(path) # Draw node dot_color = QColor(gnode.node.color) dotcolor = dot_color.lighter() pencolor = dot_color.darker() white = QColor("white") fillcolor = dotcolor #gnode.rev is None and white or dotcolor pen = QPen(pencolor) pen.setWidthF(1.5) painter.setPen(pen) radius = self.dotradius centre_x = self.col2x(gnode.node.column) centre_y = h/2 def circle(r): rect = QRectF(centre_x - r, centre_y - r, 2 * r, 2 * r) painter.drawEllipse(rect) def closesymbol(s, offset = 0): rect_ = QRectF(centre_x - 1.5 * s, centre_y - 0.5 * s, 3 * s, s) rect_.adjust(-offset, -offset, offset, offset) painter.drawRect(rect_) def diamond(r): poly = QPolygonF([QPointF(centre_x - r, centre_y), QPointF(centre_x, centre_y - r), QPointF(centre_x + r, centre_y), QPointF(centre_x, centre_y + r), QPointF(centre_x - r, centre_y),]) painter.drawPolygon(poly) nodeshape = gnode.node.status & nodestatus_shapemask if nodeshape == nodestatus_PATCH: # diamonds for patches if gnode.node.status & nodestatus_CURRENT: painter.setBrush(white) diamond(2 * 0.9 * radius / 1.5) painter.setBrush(fillcolor) diamond(radius / 1.5) elif nodeshape == nodestatus_CLOSED: if gnode.node.status & nodestatus_CURRENT: painter.setBrush(white) closesymbol(0.5 * radius, 2 * pen.widthF()) painter.setBrush(fillcolor) closesymbol(0.5 * radius) else: # circles for normal branches if gnode.node.status & nodestatus_CURRENT: painter.setBrush(white) circle(0.9 * radius) painter.setBrush(fillcolor) circle(0.5 * radius) painter.end() return QVariant(pix) class PNewDialog(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon("fileadd")) self.setWindowTitle(_('New Patch Branch')) def AddField(var, label, optional=False): hbox = QHBoxLayout() SP = QSizePolicy le = QLineEdit() le.setSizePolicy(SP(SP.Expanding, SP.Fixed)) if optional: cb = QCheckBox(label) le.setEnabled(False) cb.toggled.connect(le.setEnabled) hbox.addWidget(cb) setattr(self, var+'cb', cb) else: hbox.addWidget(QLabel(label)) hbox.addWidget(le) setattr(self, var+'le', le) return hbox def DialogButtons(): BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) bb.button(BB.Cancel).setDefault(False) self.commitButton = bb.button(BB.Ok) self.commitButton.setText(_('Commit', 'action button')) self.bb = bb return bb layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) self.setLayout(layout) layout.addLayout(AddField('patchname',_('Patch name:'))) layout.addLayout(AddField('patchtext',_('Patch message:'), optional=True)) layout.addLayout(AddField('patchdate',_('Patch date:'), optional=True)) layout.addLayout(AddField('patchuser',_('Patch user:'), optional=True)) layout.addWidget(DialogButtons()) self.patchdatele.setText( hglib.tounicode(hglib.displaytime(util.makedate()))) def patchname(self): return self.patchnamele.text() def getCmd(self): cmd = ['pnew', unicode(self.patchname())] optList = [('patchtext','--text'), ('patchdate','--date'), ('patchuser','--user')] for v,o in optList: if getattr(self,v+'cb').isChecked(): cmd.extend([o,unicode(getattr(self,v+'le').text())]) return cmd tortoisehg-2.10/tortoisehg/hgqt/__init__.py0000644000076400007640000000022212110205645020124 0ustar stevesteve# load icon resources import icons_rc # load Qt translations for frozen environment try: import translations_rc except ImportError: pass tortoisehg-2.10/tortoisehg/hgqt/revdetails.py0000644000076400007640000004125212231647662020554 0ustar stevesteve# revdetails.py - TortoiseHg revision details widget # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright (C) 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import os # for os.name from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt.filelistmodel import HgFileListModel from tortoisehg.hgqt.filelistview import HgFileListView from tortoisehg.hgqt.fileview import HgFileView from tortoisehg.hgqt.revpanel import RevPanelWidget from tortoisehg.hgqt import filectxactions, qtlib, cmdui from tortoisehg.util import hglib from PyQt4.QtCore import * from PyQt4.QtGui import * class RevDetailsWidget(QWidget, qtlib.TaskWidget): showMessage = pyqtSignal(QString) linkActivated = pyqtSignal(unicode) grepRequested = pyqtSignal(unicode, dict) revisionSelected = pyqtSignal(int) updateToRevision = pyqtSignal(int) runCustomCommandRequested = pyqtSignal(str, list) def __init__(self, repoagent, parent, rev=None): QWidget.__init__(self, parent) self._repoagent = repoagent repo = repoagent.rawRepo() # TODO: replace by repoagent if setRepo(bundlerepo) can be removed self.repo = repo self.ctx = repo[rev] self.splitternames = [] self.setupUi() self.createActions() self.setupModels() self._deschtmlize = qtlib.descriptionhtmlizer(repo.ui) repoagent.configChanged.connect(self._updatedeschtmlizer) def setRepo(self, repo): self.repo = repo self.fileview.setRepo(repo) self.filelist.setRepo(repo) self._fileactions.setRepo(repo) def setupUi(self): SP = QSizePolicy sp = SP(SP.Preferred, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(0) sp.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sp) # + basevbox -------------------------------------------------------+ # |+ filelistsplit ........ | # | + filelistframe (vbox) | + panelframe (vbox) | # | + filelisttbar | + revpanel | # +---------------------------+-------------------------------------+ # | + filelist | + messagesplitter | # | | :+ message | # | | :----------------------------------+ # | | + fileview | # +---------------------------+-------------------------------------+ basevbox = QVBoxLayout(self) basevbox.setSpacing(0) basevbox.setMargin(0) basevbox.setContentsMargins(2, 2, 2, 2) self.filelistsplit = QSplitter(self) basevbox.addWidget(self.filelistsplit) self.splitternames.append('filelistsplit') sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(0) sp.setHeightForWidth(self.filelistsplit.sizePolicy().hasHeightForWidth()) self.filelistsplit.setSizePolicy(sp) self.filelistsplit.setOrientation(Qt.Horizontal) self.filelistsplit.setChildrenCollapsible(False) self.filelisttbar = QToolBar(_('File List Toolbar')) self.filelisttbar.setIconSize(QSize(16,16)) self.filelist = HgFileListView(self.repo, self, True) self.filelist.setContextMenuPolicy(Qt.CustomContextMenu) self.filelist.customContextMenuRequested.connect(self.menuRequest) self.filelist.doubleClicked.connect(self.onDoubleClick) self.filelistframe = QFrame(self.filelistsplit) sp = SP(SP.Preferred, SP.Preferred) sp.setHorizontalStretch(3) sp.setVerticalStretch(0) sp.setHeightForWidth( self.filelistframe.sizePolicy().hasHeightForWidth()) self.filelistframe.setSizePolicy(sp) self.filelistframe.setFrameShape(QFrame.NoFrame) vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setMargin(0) vbox.addWidget(self.filelisttbar) vbox.addWidget(self.filelist) self.filelistframe.setLayout(vbox) self.fileviewframe = QFrame(self.filelistsplit) sp = SP(SP.Preferred, SP.Preferred) sp.setHorizontalStretch(7) sp.setVerticalStretch(0) sp.setHeightForWidth( self.fileviewframe.sizePolicy().hasHeightForWidth()) self.fileviewframe.setSizePolicy(sp) self.fileviewframe.setFrameShape(QFrame.NoFrame) vbox = QVBoxLayout(self.fileviewframe) vbox.setSpacing(0) vbox.setSizeConstraint(QLayout.SetDefaultConstraint) vbox.setMargin(0) panelframevbox = vbox self.messagesplitter = QSplitter(self.fileviewframe) if os.name == 'nt': self.messagesplitter.setStyle(QStyleFactory.create('Plastique')) self.splitternames.append('messagesplitter') sp = SP(SP.Preferred, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(0) sp.setHeightForWidth(self.messagesplitter.sizePolicy().hasHeightForWidth()) self.messagesplitter.setSizePolicy(sp) self.messagesplitter.setMinimumSize(QSize(50, 50)) self.messagesplitter.setFrameShape(QFrame.NoFrame) self.messagesplitter.setLineWidth(0) self.messagesplitter.setMidLineWidth(0) self.messagesplitter.setOrientation(Qt.Vertical) self.messagesplitter.setOpaqueResize(True) self.message = QTextBrowser(self.messagesplitter, lineWrapMode=QTextEdit.NoWrap, openLinks=False) self.message.minimumSizeHint = lambda: QSize(0, 25) self.message.anchorClicked.connect(self._forwardAnchorClicked) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(0) sp.setHeightForWidth(self.message.sizePolicy().hasHeightForWidth()) self.message.setSizePolicy(sp) self.message.setMinimumSize(QSize(0, 0)) self.message.sizeHint = lambda: QSize(0, 100) f = qtlib.getfont('fontcomment') self.message.setFont(f.font()) f.changed.connect(self.forwardFont) self.fileview = HgFileView(self._repoagent, self.messagesplitter) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(1) sp.setHeightForWidth(self.fileview.sizePolicy().hasHeightForWidth()) self.fileview.setSizePolicy(sp) self.fileview.setMinimumSize(QSize(0, 0)) self.fileview.linkActivated.connect(self.linkActivated) self.fileview.setFont(qtlib.getfont('fontdiff').font()) self.fileview.showMessage.connect(self.showMessage) self.fileview.grepRequested.connect(self.grepRequested) self.fileview.revisionSelected.connect(self.revisionSelected) self.filelist.fileSelected.connect(self.fileview.displayFile) self.filelist.fileSelected.connect(self.updateItemFileActions) self.filelist.clearDisplay.connect(self.fileview.clearDisplay) self.revpanel = RevPanelWidget(self.repo) self.revpanel.linkActivated.connect(self.linkActivated) panelframevbox.addWidget(self.revpanel) panelframevbox.addSpacing(5) panelframevbox.addWidget(self.messagesplitter) def forwardFont(self, font): self.message.setFont(font) def setupModels(self): self.filelistmodel = model = HgFileListModel(self) self.filelistmodel.showMessage.connect(self.showMessage) self.filelist.setModel(model) self.actionShowAllMerge.toggled.connect(model.toggleFullFileList) # Because filelist.fileSelected, i.e. currentRowChanged, is emitted # *before* selection changed, we cannot rely only on fileSelected. # On fileSelected, getSelectedFiles() happens to return the previous # selection, even though currentFile() works as expected. self.filelist.selectionModel().selectionChanged.connect( self.updateItemFileActions) def createActions(self): self.actionUpdate = a = self.filelisttbar.addAction( qtlib.geticon('hg-update'), _('Update to this revision')) a.triggered.connect(self._emitUpdateToRevision) self.filelisttbar.addSeparator() self.actionShowAllMerge = QAction(_('Show All'), self) self.actionShowAllMerge.setToolTip( _('Toggle display of all files and the direction they were merged')) self.actionShowAllMerge.setCheckable(True) self.actionShowAllMerge.setChecked(False) self.actionShowAllMerge.setEnabled(False) self.filelisttbar.addAction(self.actionShowAllMerge) le = QLineEdit() if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### filter text ###')) self.filefilter = le self.filelisttbar.addWidget(self.filefilter) self.filefilter.textEdited.connect(self.setFilter) self.actionNextLine = QAction('Next line', self) self.actionNextLine.setShortcut(Qt.SHIFT + Qt.Key_Down) self.actionNextLine.triggered.connect(self.fileview.nextLine) self.addAction(self.actionNextLine) self.actionPrevLine = QAction('Prev line', self) self.actionPrevLine.setShortcut(Qt.SHIFT + Qt.Key_Up) self.actionPrevLine.triggered.connect(self.fileview.prevLine) self.addAction(self.actionPrevLine) self.actionNextCol = QAction('Next column', self) self.actionNextCol.setShortcut(Qt.SHIFT + Qt.Key_Right) self.actionNextCol.triggered.connect(self.fileview.nextCol) self.addAction(self.actionNextCol) self.actionPrevCol = QAction('Prev column', self) self.actionPrevCol.setShortcut(Qt.SHIFT + Qt.Key_Left) self.actionPrevCol.triggered.connect(self.fileview.prevCol) self.addAction(self.actionPrevCol) self._fileactions = filectxactions.FilectxActions(self.repo, self, rev=self.ctx.rev()) self._fileactions.linkActivated.connect(self.linkActivated) self._fileactions.runCustomCommandRequested.connect( self.runCustomCommandRequested) self.addActions(self._fileactions.actions()) def onRevisionSelected(self, rev): 'called by repowidget when repoview changes revisions' self.ctx = ctx = self.repo.changectx(rev) self.revpanel.set_revision(rev) self.revpanel.update(repo = self.repo) msg = ctx.description() inlinetags = self.repo.ui.configbool('tortoisehg', 'issue.inlinetags') if ctx.tags() and inlinetags: msg = ' '.join(['[%s]' % tag for tag in ctx.tags()]) + ' ' + msg # don't use

...
, which also changes font family self.message.setHtml('
%s
' % self._deschtmlize(msg)) self._fileactions.setRev(rev) self.actionShowAllMerge.setEnabled(len(ctx.parents()) == 2) self.fileview.setContext(ctx) self.filelist.setContext(ctx) self.setFilter(self.filefilter.text()) @pyqtSlot() def _updatedeschtmlizer(self): self._deschtmlize = qtlib.descriptionhtmlizer(self.repo.ui) self.onRevisionSelected(self.ctx.rev()) # regenerate desc html def reload(self): 'Task tab is reloaded, or repowidget is refreshed' rev = self.ctx.rev() if (type(self.ctx.rev()) is int and len(self.repo) <= self.ctx.rev() or (rev is not None # wctxrev in repo raises TypeError and rev not in self.repo and rev not in self.repo.thgmqunappliedpatches)): rev = 'tip' self.onRevisionSelected(rev) @pyqtSlot(QUrl) def _forwardAnchorClicked(self, url): self.linkActivated.emit(url.toString()) @pyqtSlot() def _emitUpdateToRevision(self): self.updateToRevision.emit(self.ctx.rev()) #@pyqtSlot(QModelIndex) def onDoubleClick(self, index): model = self.filelist.model() itemstatus = model.dataFromIndex(index)['status'] itemissubrepo = (itemstatus == 'S') if itemissubrepo: self._fileactions.opensubrepo() elif itemstatus == 'C': self._fileactions.editfile() else: self._fileactions.vdiff() @pyqtSlot(QPoint) def menuRequest(self, point): index = self.filelist.currentIndex() if not index.isValid(): return model = self.filelist.model() data = model.dataFromIndex(index) if not data: return contextmenu = self._fileactions.menu() if contextmenu: contextmenu.exec_(self.filelist.viewport().mapToGlobal(point)) @pyqtSlot() def updateItemFileActions(self): index = self.filelist.currentIndex() model = self.filelist.model() data = model.dataFromIndex(index) if not data: return itemissubrepo = (data['status'] == 'S') self._fileactions.setPaths_(self.filelist.getSelectedFiles(), self.filelist.currentFile(), itemissubrepo) @pyqtSlot(QString) def setFilter(self, match): model = self.filelist.model() if model is not None: model.setFilter(match) self.filelist.enablefilterpalette(bool(match)) def saveSettings(self, s): wb = "RevDetailsWidget/" for n in self.splitternames: s.setValue(wb + n, getattr(self, n).saveState()) s.setValue(wb + 'revpanel.expanded', self.revpanel.is_expanded()) self.fileview.saveSettings(s, 'revpanel/fileview') def loadSettings(self, s): wb = "RevDetailsWidget/" for n in self.splitternames: getattr(self, n).restoreState(s.value(wb + n).toByteArray()) expanded = s.value(wb + 'revpanel.expanded', False).toBool() self.revpanel.set_expanded(expanded) self.fileview.loadSettings(s, 'revpanel/fileview') class RevDetailsDialog(QDialog): 'Standalone revision details tool, a wrapper for RevDetailsWidget' def __init__(self, repoagent, rev='.', parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon('hg-log')) layout = QVBoxLayout() layout.setMargin(0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setContentsMargins(5, 5, 5, 0) layout.addLayout(toplayout) revdetails = RevDetailsWidget(repoagent, parent, rev=rev) toplayout.addWidget(revdetails, 1) self.statusbar = cmdui.ThgStatusBar(self) revdetails.showMessage.connect(self.statusbar.showMessage) revdetails.linkActivated.connect(self.linkActivated) layout.addWidget(self.statusbar) s = QSettings() self.restoreGeometry(s.value('revdetails/geom').toByteArray()) revdetails.loadSettings(s) repoagent.repositoryChanged.connect(self.refresh) self.revdetails = revdetails self.setRev(rev) qtlib.newshortcutsforstdkey(QKeySequence.Refresh, self, self.refresh) def setRev(self, rev): self.revdetails.onRevisionSelected(rev) self.refresh() def linkActivated(self, link): link = hglib.fromunicode(link) link = link.split(':', 1) if len(link) == 1: linktype = 'cset:' linktarget = link[0] else: linktype = link[0] linktarget = link[1] if linktype == 'cset': self.setRev(linktarget) elif linktype == 'repo': try: linkpath, rev = linktarget.split('?', 1) except ValueError: linkpath = linktarget rev = None # TODO: implement by using signal-slot if possible from tortoisehg.hgqt import run run.qtrun.showRepoInWorkbench(hglib.tounicode(linkpath), rev) @pyqtSlot() def refresh(self): rev = revnum = self.revdetails.ctx.rev() if rev is None: revstr = _('Working Directory') else: hash = self.revdetails.ctx.hex()[:12] revstr = '@%s: %s' % (str(revnum), hash) self.setWindowTitle(_('%s - Revision Details (%s)') % (self.revdetails.repo.displayname, revstr)) self.revdetails.reload() def done(self, ret): s = QSettings() s.setValue('revdetails/geom', self.saveGeometry()) super(RevDetailsDialog, self).done(ret) tortoisehg-2.10/tortoisehg/hgqt/visdiff.py0000644000076400007640000005223112170335562020036 0ustar stevesteve# visdiff.py - launch external visual diff tools # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import subprocess import stat import shutil import threading import tempfile import re from mercurial import hg, util, error, match, scmutil, copies from tortoisehg.hgqt.i18n import _ from tortoisehg.util import hglib from tortoisehg.hgqt import qtlib from PyQt4.QtCore import * from PyQt4.QtGui import * # Match parent2 first, so 'parent1?' will match both parent1 and parent _regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|repo|phash1|phash2|chash)' _nonexistant = _('[non-existant]') # This global counter is incremented for each visual diff done in a session # It ensures that the names for snapshots created do not collide. _diffCount = 0 def snapshotset(repo, ctxs, sa, sb, copies, copyworkingdir = False): '''snapshot files from parent-child set of revisions''' ctx1a, ctx1b, ctx2 = ctxs mod_a, add_a, rem_a = sa mod_b, add_b, rem_b = sb global _diffCount _diffCount += 1 if copies: sources = set(copies.values()) else: sources = set() # Always make a copy of ctx1a files1a = sources | mod_a | rem_a | ((mod_b | add_b) - add_a) dir1a, fns_mtime1a = snapshot(repo, files1a, ctx1a) label1a = '@%d:%s' % (ctx1a.rev(), ctx1a) # Make a copy of ctx1b if relevant if ctx1b: files1b = sources | mod_b | rem_b | ((mod_a | add_a) - add_b) dir1b, fns_mtime1b = snapshot(repo, files1b, ctx1b) label1b = '@%d:%s' % (ctx1b.rev(), ctx1b) else: dir1b = None fns_mtime1b = [] label1b = '' # Either make a copy of ctx2, or use working dir directly if relevant. files2 = mod_a | add_a | mod_b | add_b if ctx2.rev() is None: if copyworkingdir: dir2, fns_mtime2 = snapshot(repo, files2, ctx2) else: dir2 = repo.root fns_mtime2 = [] # If ctx2 is working copy, use empty label. label2 = '' else: dir2, fns_mtime2 = snapshot(repo, files2, ctx2) label2 = '@%d:%s' % (ctx2.rev(), ctx2) dirs = [dir1a, dir1b, dir2] labels = [label1a, label1b, label2] fns_and_mtimes = [fns_mtime1a, fns_mtime1b, fns_mtime2] return dirs, labels, fns_and_mtimes def snapshot(repo, files, ctx): '''snapshot files as of some revision''' dirname = os.path.basename(repo.root) or 'root' dirname += '.%d' % _diffCount if ctx.rev() is not None: dirname += '.%d' % ctx.rev() base = os.path.join(qtlib.gettempdir(), dirname) fns_and_mtime = [] if not os.path.exists(base): os.makedirs(base) for fn in files: wfn = util.pconvert(fn) if not wfn in ctx: # File doesn't exist; could be a bogus modify continue dest = os.path.join(base, wfn) if os.path.exists(dest): # File has already been snapshot continue destdir = os.path.dirname(dest) try: if not os.path.isdir(destdir): os.makedirs(destdir) data = repo.wwritedata(wfn, ctx[wfn].data()) f = open(dest, 'wb') f.write(data) f.close() if ctx.rev() is None: fns_and_mtime.append((dest, repo.wjoin(fn), os.lstat(dest).st_mtime)) else: # Make file read/only, to indicate it's static (archival) nature os.chmod(dest, stat.S_IREAD) except EnvironmentError: pass return base, fns_and_mtime def launchtool(cmd, opts, replace, block): def quote(match): key = match.group()[1:] return util.shellquote(replace[key]) if isinstance(cmd, unicode): cmd = hglib.fromunicode(cmd) lopts = [] for opt in opts: if isinstance(opt, unicode): lopts.append(hglib.fromunicode(opt)) else: lopts.append(opt) args = ' '.join(lopts) args = re.sub(_regex, quote, args) cmdline = util.shellquote(cmd) + ' ' + args cmdline = util.quotecommand(cmdline) try: proc = subprocess.Popen(cmdline, shell=True, creationflags=qtlib.openflags, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) if block: proc.communicate() except (OSError, EnvironmentError), e: QMessageBox.warning(None, _('Tool launch failure'), _('%s : %s') % (cmd, str(e))) def filemerge(ui, fname, patchedfname): 'Launch the preferred visual diff tool for two text files' detectedtools = hglib.difftools(ui) if not detectedtools: QMessageBox.warning(None, _('No diff tool found'), _('No visual diff tools were detected')) return None preferred = besttool(ui, detectedtools) diffcmd, diffopts, mergeopts = detectedtools[preferred] replace = dict(parent=fname, parent1=fname, plabel1=fname + _('[working copy]'), repo='', phash1='', phash2='', chash='', child=patchedfname, clabel=_('[original]')) launchtool(diffcmd, diffopts, replace, True) def besttool(ui, tools, force=None): 'Select preferred or highest priority tool from dictionary' preferred = force or ui.config('tortoisehg', 'vdiff') or \ ui.config('ui', 'merge') if preferred and preferred in tools: return preferred pris = [] for t in tools.keys(): p = int(ui.config('merge-tools', t + '.priority', 0)) pris.append((-p, t)) tools = sorted(pris) return tools[0][1] def visualdiff(ui, repo, pats, opts): revs = opts.get('rev', []) change = opts.get('change') try: ctx1b = None if change: ctx2 = repo[change] p = ctx2.parents() if len(p) > 1: ctx1a, ctx1b = p else: ctx1a = p[0] else: n1, n2 = scmutil.revpair(repo, revs) ctx1a, ctx2 = repo[n1], repo[n2] p = ctx2.parents() if not revs and len(p) > 1: ctx1b = p[1] except (error.LookupError, error.RepoError): QMessageBox.warning(None, _('Unable to find changeset'), _('You likely need to refresh this application')) return None pats = scmutil.expandpats(pats) m = match.match(repo.root, '', pats, None, None, 'relpath') n2 = ctx2.node() mod_a, add_a, rem_a = map(set, repo.status(ctx1a.node(), n2, m)[:3]) if ctx1b: mod_b, add_b, rem_b = map(set, repo.status(ctx1b.node(), n2, m)[:3]) cpy = copies.mergecopies(repo, ctx1a, ctx1b, ctx1a.ancestor(ctx1b))[0] else: cpy = copies.pathcopies(ctx1a, ctx2) mod_b, add_b, rem_b = set(), set(), set() MA = mod_a | add_a | mod_b | add_b MAR = MA | rem_a | rem_b if not MAR: QMessageBox.information(None, _('No file changes'), _('There are no file changes to view')) return None detectedtools = hglib.difftools(repo.ui) if not detectedtools: QMessageBox.warning(None, _('No diff tool found'), _('No visual diff tools were detected')) return None preferred = besttool(repo.ui, detectedtools, opts.get('tool')) # Build tool list based on diff-patterns matches toollist = set() patterns = repo.ui.configitems('diff-patterns') patterns = [(p, t) for p,t in patterns if t in detectedtools] for path in MAR: for pat, tool in patterns: mf = match.match(repo.root, '', [pat]) if mf(path): toollist.add(tool) break else: toollist.add(preferred) cto = cpy.keys() for path in MAR: if path in cto: hascopies = True break else: hascopies = False force = repo.ui.configbool('tortoisehg', 'forcevdiffwin') if len(toollist) > 1 or (hascopies and len(MAR) > 1) or force: usewin = True else: preferred = toollist.pop() dirdiff = repo.ui.configbool('merge-tools', preferred + '.dirdiff') dir3diff = repo.ui.configbool('merge-tools', preferred + '.dir3diff') usewin = repo.ui.configbool('merge-tools', preferred + '.usewin') if not usewin and len(MAR) > 1: if ctx1b is not None: usewin = not dir3diff else: usewin = not dirdiff if usewin: # Multiple required tools, or tool does not support directory diffs sa = [mod_a, add_a, rem_a] sb = [mod_b, add_b, rem_b] dlg = FileSelectionDialog(repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy) return dlg # We can directly use the selected tool, without a visual diff window diffcmd, diffopts, mergeopts = detectedtools[preferred] # Disable 3-way merge if there is only one parent or no tool support do3way = False if ctx1b: if mergeopts: do3way = True args = mergeopts else: args = diffopts if str(ctx1b.rev()) in revs: ctx1a = ctx1b else: args = diffopts def dodiff(): assert not (hascopies and len(MAR) > 1), \ 'dodiff cannot handle copies when diffing dirs' sa = [mod_a, add_a, rem_a] sb = [mod_b, add_b, rem_b] ctxs = [ctx1a, ctx1b, ctx2] # If more than one file, diff on working dir copy. copyworkingdir = len(MAR) > 1 dirs, labels, fns_and_mtimes = snapshotset(repo, ctxs, sa, sb, cpy, copyworkingdir) dir1a, dir1b, dir2 = dirs label1a, label1b, label2 = labels fns_and_mtime = fns_and_mtimes[2] if len(MAR) > 1 and label2 == '': label2 = 'working files' def getfile(fname, dir, label): file = os.path.join(qtlib.gettempdir(), dir, fname) if os.path.isfile(file): return fname+label, file nullfile = os.path.join(qtlib.gettempdir(), 'empty') fp = open(nullfile, 'w') fp.close() return (hglib.fromunicode(_nonexistant, 'replace') + label, nullfile) # If only one change, diff the files instead of the directories # Handle bogus modifies correctly by checking if the files exist if len(MAR) == 1: file2 = MAR.pop() file2local = util.localpath(file2) if file2 in cto: file1 = util.localpath(cpy[file2]) else: file1 = file2 label1a, dir1a = getfile(file1, dir1a, label1a) if do3way: label1b, dir1b = getfile(file1, dir1b, label1b) label2, dir2 = getfile(file2local, dir2, label2) if do3way: label1a += '[local]' label1b += '[other]' label2 += '[merged]' replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, plabel1=label1a, plabel2=label1b, phash1=str(ctx1a), phash2=str(ctx1b), repo=hglib.fromunicode(repo.displayname), clabel=label2, child=dir2, chash=str(ctx2)) launchtool(diffcmd, args, replace, True) # detect if changes were made to mirrored working files for copy_fn, working_fn, mtime in fns_and_mtime: try: if os.lstat(copy_fn).st_mtime != mtime: ui.debug('file changed while diffing. ' 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)) util.copyfile(copy_fn, working_fn) except EnvironmentError: pass # Ignore I/O errors or missing files def dodiffwrapper(): try: dodiff() finally: # cleanup happens atexit ui.note('cleaning up temp directory\n') if opts.get('mainapp'): dodiffwrapper() else: # We are not the main application, so this must be done in a # background thread thread = threading.Thread(target=dodiffwrapper, name='visualdiff') thread.setDaemon(True) thread.start() class FileSelectionDialog(QDialog): 'Dialog for selecting visual diff candidates' def __init__(self, repo, pats, ctx1a, sa, ctx1b, sb, ctx2, cpy): 'Initialize the Dialog' QDialog.__init__(self) self.setWindowIcon(qtlib.geticon('visualdiff')) if ctx2.rev() is None: title = _('working changes') elif ctx1a == ctx2.parents()[0]: title = _('changeset %d:%s') % (ctx2.rev(), ctx2) else: title = _('revisions %d:%s to %d:%s') \ % (ctx1a.rev(), ctx1a, ctx2.rev(), ctx2) title = _('Visual Diffs - ') + title if pats: title += _(' filtered') self.setWindowTitle(title) self.resize(650, 250) self.reponame = hglib.fromunicode(repo.displayname) self.ctxs = (ctx1a, ctx1b, ctx2) self.filesets = (sa, sb) self.copies = cpy self.repo = repo self.curFile = None layout = QVBoxLayout() self.setLayout(layout) lbl = QLabel(_('Temporary files are removed when this dialog ' 'is closed')) layout.addWidget(lbl) list = QListWidget() layout.addWidget(list) self.list = list list.itemActivated.connect(self.itemActivated) tools = hglib.difftools(repo.ui) preferred = besttool(repo.ui, tools) self.diffpath, self.diffopts, self.mergeopts = tools[preferred] self.tools = tools self.preferred = preferred if len(tools) > 1: hbox = QHBoxLayout() combo = QComboBox() lbl = QLabel(_('Select Tool:')) lbl.setBuddy(combo) hbox.addWidget(lbl) hbox.addWidget(combo, 1) layout.addLayout(hbox) for i, name in enumerate(tools.iterkeys()): combo.addItem(name) if name == preferred: defrow = i combo.setCurrentIndex(defrow) list.currentRowChanged.connect(self.updateToolSelection) combo.currentIndexChanged['QString'].connect(self.onToolSelected) self.toolCombo = combo BB = QDialogButtonBox bb = BB() layout.addWidget(bb) if ctx2.rev() is None: pass # Do not offer directory diffs when the working directory # is being referenced directly elif ctx1b: self.p1button = bb.addButton(_('Dir diff to p1'), BB.ActionRole) self.p1button.pressed.connect(self.p1dirdiff) self.p2button = bb.addButton(_('Dir diff to p2'), BB.ActionRole) self.p2button.pressed.connect(self.p2dirdiff) self.p3button = bb.addButton(_('3-way dir diff'), BB.ActionRole) self.p3button.pressed.connect(self.threewaydirdiff) else: self.dbutton = bb.addButton(_('Directory diff'), BB.ActionRole) self.dbutton.pressed.connect(self.p1dirdiff) self.updateDiffButtons(preferred) QShortcut(QKeySequence('CTRL+D'), self.list, self.activateCurrent) QTimer.singleShot(0, self.fillmodel) @pyqtSlot() def fillmodel(self): repo = self.repo sa, sb = self.filesets self.dirs, self.revs = snapshotset(repo, self.ctxs, sa, sb, self.copies)[:2] def get_status(file, mod, add, rem): if file in mod: return 'M' if file in add: return 'A' if file in rem: return 'R' return ' ' mod_a, add_a, rem_a = sa for f in sorted(mod_a | add_a | rem_a): status = get_status(f, mod_a, add_a, rem_a) row = QString('%s %s' % (status, hglib.tounicode(f))) self.list.addItem(row) @pyqtSlot(QString) def onToolSelected(self, tool): 'user selected a tool from the tool combo' tool = hglib.fromunicode(tool) assert tool in self.tools self.diffpath, self.diffopts, self.mergeopts = self.tools[tool] self.updateDiffButtons(tool) @pyqtSlot(int) def updateToolSelection(self, row): 'user selected a file, pick an appropriate tool from combo' if row == -1: return repo = self.repo patterns = repo.ui.configitems('diff-patterns') patterns = [(p, t) for p,t in patterns if t in self.tools] fname = self.list.item(row).text()[2:] fname = hglib.fromunicode(fname) if self.curFile == fname: return self.curFile = fname for pat, tool in patterns: mf = match.match(repo.root, '', [pat]) if mf(fname): selected = tool break else: selected = self.preferred for i, name in enumerate(self.tools.iterkeys()): if name == selected: self.toolCombo.setCurrentIndex(i) def activateCurrent(self): 'CTRL+D has been pressed' row = self.list.currentRow() if row >= 0: self.launch(self.list.item(row).text()[2:]) def itemActivated(self, item): 'A QListWidgetItem has been activated' self.launch(item.text()[2:]) def updateDiffButtons(self, tool): if hasattr(self, 'p1button'): d2 = self.repo.ui.configbool('merge-tools', tool + '.dirdiff') d3 = self.repo.ui.configbool('merge-tools', tool + '.dir3diff') self.p1button.setEnabled(d2) self.p2button.setEnabled(d2) self.p3button.setEnabled(d3) elif hasattr(self, 'dbutton'): d2 = self.repo.ui.configbool('merge-tools', tool + '.dirdiff') self.dbutton.setEnabled(d2) def launch(self, fname): fname = hglib.fromunicode(fname) source = self.copies.get(fname, None) dir1a, dir1b, dir2 = self.dirs rev1a, rev1b, rev2 = self.revs ctx1a, ctx1b, ctx2 = self.ctxs def getfile(ctx, dir, fname, source): m = ctx.manifest() if fname in m: path = os.path.join(dir, util.localpath(fname)) return fname, path elif source and source in m: path = os.path.join(dir, util.localpath(source)) return source, path else: nullfile = os.path.join(qtlib.gettempdir(), 'empty') fp = open(nullfile, 'w') fp.close() return hglib.fromunicode(_nonexistant, 'replace'), nullfile local, file1a = getfile(ctx1a, dir1a, fname, source) if ctx1b: other, file1b = getfile(ctx1b, dir1b, fname, source) else: other, file1b = fname, None fname, file2 = getfile(ctx2, dir2, fname, None) label1a = local+rev1a label1b = other+rev1b label2 = fname+rev2 if ctx1b: label1a += '[local]' label1b += '[other]' label2 += '[merged]' # Function to quote file/dir names in the argument string replace = dict(parent=file1a, parent1=file1a, plabel1=label1a, parent2=file1b, plabel2=label1b, repo=self.reponame, phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), clabel=label2, child=file2) args = ctx1b and self.mergeopts or self.diffopts launchtool(self.diffpath, args, replace, False) def p1dirdiff(self): dir1a, dir1b, dir2 = self.dirs rev1a, rev1b, rev2 = self.revs ctx1a, ctx1b, ctx2 = self.ctxs replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a, repo=self.reponame, phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), parent2='', plabel2='', clabel=rev2, child=dir2) launchtool(self.diffpath, self.diffopts, replace, False) def p2dirdiff(self): dir1a, dir1b, dir2 = self.dirs rev1a, rev1b, rev2 = self.revs ctx1a, ctx1b, ctx2 = self.ctxs replace = dict(parent=dir1b, parent1=dir1b, plabel1=rev1b, repo=self.reponame, phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), parent2='', plabel2='', clabel=rev2, child=dir2) launchtool(self.diffpath, self.diffopts, replace, False) def threewaydirdiff(self): dir1a, dir1b, dir2 = self.dirs rev1a, rev1b, rev2 = self.revs ctx1a, ctx1b, ctx2 = self.ctxs replace = dict(parent=dir1a, parent1=dir1a, plabel1=rev1a, repo=self.reponame, phash1=str(ctx1a), phash2=str(ctx1b), chash=str(ctx2), parent2=dir1b, plabel2=rev1b, clabel=dir2, child=rev2) launchtool(self.diffpath, self.mergeopts, replace, False) tortoisehg-2.10/tortoisehg/hgqt/webconf_ui.py0000644000076400007640000000745012212224146020517 0ustar stevesteve# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/steve/repos/thg/tortoisehg/hgqt/webconf.ui' # # Created: Thu Sep 5 19:57:10 2013 # by: PyQt4 UI code generator 4.6.2 # # WARNING! All changes made in this file will be lost! from tortoisehg.hgqt.i18n import _ from PyQt4 import QtCore, QtGui class Ui_WebconfForm(object): def setupUi(self, WebconfForm): WebconfForm.setObjectName("WebconfForm") WebconfForm.resize(455, 300) self.form_layout = QtGui.QVBoxLayout(WebconfForm) self.form_layout.setObjectName("form_layout") self.path_layout = QtGui.QHBoxLayout() self.path_layout.setObjectName("path_layout") self.path_label = QtGui.QLabel(WebconfForm) self.path_label.setObjectName("path_label") self.path_layout.addWidget(self.path_label) self.path_edit = QtGui.QComboBox(WebconfForm) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.path_edit.sizePolicy().hasHeightForWidth()) self.path_edit.setSizePolicy(sizePolicy) self.path_edit.setInsertPolicy(QtGui.QComboBox.InsertAtTop) self.path_edit.setObjectName("path_edit") self.path_layout.addWidget(self.path_edit) self.open_button = QtGui.QToolButton(WebconfForm) self.open_button.setObjectName("open_button") self.path_layout.addWidget(self.open_button) self.save_button = QtGui.QToolButton(WebconfForm) self.save_button.setObjectName("save_button") self.path_layout.addWidget(self.save_button) self.form_layout.addLayout(self.path_layout) self.filerepos_sep = QtGui.QFrame(WebconfForm) self.filerepos_sep.setFrameShape(QtGui.QFrame.HLine) self.filerepos_sep.setFrameShadow(QtGui.QFrame.Sunken) self.filerepos_sep.setObjectName("filerepos_sep") self.form_layout.addWidget(self.filerepos_sep) self.repos_layout = QtGui.QHBoxLayout() self.repos_layout.setObjectName("repos_layout") self.repos_view = QtGui.QTreeView(WebconfForm) self.repos_view.setIndentation(0) self.repos_view.setRootIsDecorated(False) self.repos_view.setItemsExpandable(False) self.repos_view.setObjectName("repos_view") self.repos_layout.addWidget(self.repos_view) self.addremove_layout = QtGui.QVBoxLayout() self.addremove_layout.setObjectName("addremove_layout") self.add_button = QtGui.QToolButton(WebconfForm) self.add_button.setObjectName("add_button") self.addremove_layout.addWidget(self.add_button) self.edit_button = QtGui.QToolButton(WebconfForm) self.edit_button.setObjectName("edit_button") self.addremove_layout.addWidget(self.edit_button) self.remove_button = QtGui.QToolButton(WebconfForm) self.remove_button.setObjectName("remove_button") self.addremove_layout.addWidget(self.remove_button) spacerItem = QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.addremove_layout.addItem(spacerItem) self.repos_layout.addLayout(self.addremove_layout) self.form_layout.addLayout(self.repos_layout) self.path_label.setBuddy(self.path_edit) self.retranslateUi(WebconfForm) QtCore.QMetaObject.connectSlotsByName(WebconfForm) def retranslateUi(self, WebconfForm): WebconfForm.setWindowTitle(_('Webconf')) self.path_label.setText(_('Config File:')) self.open_button.setText(_('Open')) self.save_button.setText(_('Save')) self.add_button.setText(_('Add')) self.edit_button.setText(_('Edit')) self.remove_button.setText(_('Remove')) tortoisehg-2.10/tortoisehg/hgqt/wctxcleaner.py0000644000076400007640000001026112231647662020725 0ustar stevesteve# wctxcleaner.py - check and clean dirty working directory # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import QObject, QThread from PyQt4.QtCore import pyqtSignal, pyqtSlot from PyQt4.QtGui import QMessageBox, QWidget from mercurial import hg from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, cmdui, qtlib, thgrepo class CheckThread(QThread): def __init__(self, repo, parent): QThread.__init__(self, parent) self.repo = hg.repository(repo.ui, repo.root) self.results = (False, 1) self.canceled = False def run(self): self.repo.dirstate.invalidate() unresolved = False for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if self.canceled: return if status == 'u': unresolved = True break wctx = self.repo[None] try: dirty = bool(wctx.dirty()) or unresolved self.results = (dirty, len(wctx.parents())) except EnvironmentError: self.results = (True, len(wctx.parents())) def cancel(self): self.canceled = True class WctxCleaner(QObject): checkStarted = pyqtSignal() checkFinished = pyqtSignal(bool, int) # clean, parents def __init__(self, repoagent, parent=None): super(WctxCleaner, self).__init__(parent) assert parent is None or isinstance(parent, QWidget) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._checkth = CheckThread(repoagent.rawRepo(), self) self._checkth.started.connect(self.checkStarted) self._checkth.finished.connect(self._onCheckFinished) self._clean = False @pyqtSlot() def check(self): """Check states of working directory asynchronously""" if self._checkth.isRunning(): return self._checkth.start() def cancelCheck(self): self._checkth.cancel() self._checkth.wait() def isChecking(self): return self._checkth.isRunning() def isCheckCanceled(self): return self._checkth.canceled def isClean(self): return self._clean @pyqtSlot() def _onCheckFinished(self): dirty, parents = self._checkth.results self._clean = not dirty self.checkFinished.emit(not dirty, parents) @pyqtSlot(str) def runCleaner(self, cmd): """Clean working directory by the specified action""" cmd = str(cmd) if cmd == 'commit': self.launchCommitDialog() elif cmd == 'shelve': self.launchShelveDialog() elif cmd.startswith('discard'): confirm = cmd != 'discard:noconfirm' self.discardChanges(confirm) else: raise ValueError('unknown command: %s' % cmd) def launchCommitDialog(self): from tortoisehg.hgqt import commit dlg = commit.CommitDialog(self._repoagent, [], {}, self.parent()) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.check() def launchShelveDialog(self): from tortoisehg.hgqt import shelve dlg = shelve.ShelveDialog(self._repoagent, self.parent()) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.check() def discardChanges(self, confirm=True): if confirm: labels = [(QMessageBox.Yes, _('&Discard')), (QMessageBox.No, _('Cancel'))] if not qtlib.QuestionMsgBox(_('Confirm Discard'), _('Discard outstanding changes to working directory?'), labels=labels, parent=self.parent()): return cmdline = ['update', '--clean', '--rev', '.'] self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onCommandFinished) @pyqtSlot(int) def _onCommandFinished(self, ret): if ret == 0: self.check() else: cmdui.errorMessageBox(self._cmdsession, self.parent()) tortoisehg-2.10/tortoisehg/hgqt/htmlui.py0000644000076400007640000000622712110205646017703 0ustar stevesteve# htmlui.py - mercurial.ui.ui class which emits HTML/Rich Text # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os, cgi, time from mercurial import ui from tortoisehg.hgqt import qtlib from tortoisehg.util import hglib BEGINTAG = '\033' + str(time.time()) ENDTAG = '\032' + str(time.time()) class htmlui(ui.ui): def __init__(self, src=None): super(htmlui, self).__init__(src) self.setconfig('ui', 'interactive', 'off') self.setconfig('progress', 'disable', 'True') self.output, self.error = [], [] def write(self, *args, **opts): label = opts.get('label', '') if self._buffers: self._buffers[-1].extend([(str(a), label) for a in args]) else: self.output.extend(self.smartlabel(''.join(args), label)) def write_err(self, *args, **opts): label = opts.get('label', 'ui.error') self.error.extend(self.smartlabel(''.join(args), label)) def label(self, msg, label): ''' Called by Mercurial to apply styling (formatting) to a piece of text. Our implementation wraps tags around the data so we can find it later when it is passed to ui.write() ''' return BEGINTAG + self.style(msg, label) + ENDTAG def style(self, msg, label): 'Escape message for safe HTML, then apply specified style' msg = cgi.escape(msg).replace('\n', '
') style = qtlib.geteffect(label) return '%s' % (style, msg) def smartlabel(self, text, label): ''' Escape and apply style, excluding any text between BEGINTAG and ENDTAG. That text has already been escaped and styled. ''' parts = [] try: while True: b = text.index(BEGINTAG) e = text.index(ENDTAG) if e > b: if b: parts.append(self.style(text[:b], label)) parts.append(text[b + len(BEGINTAG):e]) text = text[e + len(ENDTAG):] else: # invalid range, assume ENDTAG and BEGINTAG # are naturually occuring. Style, append, and # consume up to the BEGINTAG and repeat. parts.append(self.style(text[:b], label)) text = text[b:] except ValueError: pass if text: parts.append(self.style(text, label)) return parts def popbuffer(self, labeled=False): b = self._buffers.pop() if labeled: return ''.join(self.style(a, label) for a, label in b) return ''.join(a for a, label in b) def plain(self, feature=None): return True def getdata(self): d, e = ''.join(self.output), ''.join(self.error) self.output, self.error = [], [] return d, e if __name__ == "__main__": from mercurial import hg u = htmlui() repo = hg.repository(u) repo.status() print u.getdata()[0] tortoisehg-2.10/tortoisehg/hgqt/repotreemodel.py0000644000076400007640000003443512231647662021265 0ustar stevesteve# repotreemodel.py - model for the reporegistry # # Copyright 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import repotreeitem from PyQt4.QtCore import * from PyQt4.QtGui import QFont import os if PYQT_VERSION < 0x40700: class LocalQXmlStreamReader(QXmlStreamReader): def readNextStartElement(self): while self.readNext() != QXmlStreamReader.Invalid: if self.isEndElement(): return False elif self.isStartElement(): return True return False def skipCurrentElement(self): depth = 1 while depth > 0 and self.readNext() != QXmlStreamReader.Invalid: if self.isEndElement(): depth -= 1 elif self.isStartElement(): depth += 1 QXmlStreamReader = LocalQXmlStreamReader extractXmlElementName = 'reporegextract' reporegistryXmlElementName = 'reporegistry' repoRegMimeType = 'application/thg-reporegistry' repoExternalMimeType = 'text/uri-list' def writeXml(target, item, rootElementName): xw = QXmlStreamWriter(target) xw.setAutoFormatting(True) xw.setAutoFormattingIndent(2) xw.writeStartDocument() xw.writeStartElement(rootElementName) item.dumpObject(xw) xw.writeEndElement() xw.writeEndDocument() def readXml(source, rootElementName): itemread = None xr = QXmlStreamReader(source) if xr.readNextStartElement(): ele = str(xr.name().toString()) if ele != rootElementName: print "unexpected xml element '%s' "\ "(was looking for %s)" % (ele, rootElementName) return if xr.hasError(): print str(xr.errorString()) if xr.readNextStartElement(): itemread = repotreeitem.undumpObject(xr) xr.skipCurrentElement() if xr.hasError(): print str(xr.errorString()) return itemread def iterRepoItemFromXml(source): 'Used by thgrepo.relatedRepositories to scan the XML file' xr = QXmlStreamReader(source) while not xr.atEnd(): t = xr.readNext() if (t == QXmlStreamReader.StartElement and xr.name() in ('repo', 'subrepo')): yield repotreeitem.undumpObject(xr) def getRepoItemList(root, standalone=False): if standalone: stopfunc = lambda e: isinstance(e, repotreeitem.RepoItem) else: stopfunc = None return [e for e in repotreeitem.flatten(root, stopfunc=stopfunc) if isinstance(e, repotreeitem.RepoItem)] class RepoTreeModel(QAbstractItemModel): def __init__(self, filename, repomanager, parent=None, showShortPaths=False): QAbstractItemModel.__init__(self, parent) self._repomanager = repomanager self._repomanager.configChanged.connect(self._updateShortName) self._repomanager.repositoryChanged.connect(self._updateBaseNode) self._repomanager.repositoryOpened.connect(self._updateItem) self.showShortPaths = showShortPaths self._activeRepoItem = None root = None if filename: f = QFile(filename) if f.open(QIODevice.ReadOnly): root = readXml(f, reporegistryXmlElementName) f.close() if not root: root = repotreeitem.RepoTreeItem(self) # due to issue #1075, 'all' may be missing even if 'root' exists try: all = repotreeitem.find( root, lambda e: isinstance(e, repotreeitem.AllRepoGroupItem)) except ValueError: all = repotreeitem.AllRepoGroupItem() root.appendChild(all) self.rootItem = root self.allrepos = all self.updateCommonPaths() # see http://doc.qt.nokia.com/4.6/model-view-model-subclassing.html # overrides from QAbstractItemModel def index(self, row, column, parent=QModelIndex()): if not self.hasIndex(row, column, parent): return QModelIndex() if (not parent.isValid()): parentItem = self.rootItem else: parentItem = parent.internalPointer() childItem = parentItem.child(row) if childItem: return self.createIndex(row, column, childItem) else: return QModelIndex() def parent(self, index): if not index.isValid(): return QModelIndex() childItem = index.internalPointer() parentItem = childItem.parent() if parentItem is self.rootItem: return QModelIndex() return self.createIndex(parentItem.row(), 0, parentItem) def rowCount(self, parent=QModelIndex()): if parent.column() > 0: return 0 if not parent.isValid(): parentItem = self.rootItem else: parentItem = parent.internalPointer() return parentItem.childCount() def columnCount(self, parent=QModelIndex()): if parent.isValid(): return parent.internalPointer().columnCount() else: return self.rootItem.columnCount() def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role not in (Qt.DisplayRole, Qt.EditRole, Qt.DecorationRole, Qt.FontRole): return QVariant() item = index.internalPointer() if role == Qt.FontRole and item is self._activeRepoItem: font = QFont() font.setBold(True) return font else: return item.data(index.column(), role) def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: if section == 1: return QString(_('Path')) return QVariant() def flags(self, index): if not index.isValid(): return Qt.NoItemFlags item = index.internalPointer() return item.flags() def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction | Qt.LinkAction def removeRows(self, row, count, parent=QModelIndex()): item = parent.internalPointer() if item is None: item = self.rootItem if count <= 0 or row < 0 or row + count > item.childCount(): return False self.beginRemoveRows(parent, row, row+count-1) if self._activeRepoItem in item.childs[row:row + count]: self._activeRepoItem = None res = item.removeRows(row, count) self.endRemoveRows() return res def mimeTypes(self): return QStringList([repoRegMimeType, repoExternalMimeType]) def mimeData(self, indexes): i = indexes[0] item = i.internalPointer() buf = QByteArray() writeXml(buf, item, extractXmlElementName) d = QMimeData() d.setData(repoRegMimeType, buf) if isinstance(item, repotreeitem.RepoItem): d.setUrls([QUrl.fromLocalFile(hglib.tounicode(item.rootpath()))]) else: d.setText(QString(item.name)) return d def dropMimeData(self, data, action, row, column, parent): group = parent.internalPointer() d = str(data.data(repoRegMimeType)) if not data.hasUrls(): # The source is a group if row < 0: # The group has been dropped on a group # In that case, place the group at the same level as the target # group row = parent.row() parent = parent.parent() group = parent.internalPointer() if row < 0 or not isinstance(group, repotreeitem.RepoGroupItem): # The group was dropped at the top level group = self.rootItem parent = QModelIndex() itemread = readXml(d, extractXmlElementName) if itemread is None: return False if group is None: return False # Avoid copying subrepos multiple times if Qt.CopyAction == action and self.getRepoItem(itemread.rootpath()): return False if row < 0: row = 0 self.beginInsertRows(parent, row, row) group.insertChild(row, itemread) self.endInsertRows() if isinstance(itemread, repotreeitem.AllRepoGroupItem): self.allrepos = itemread return True def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False s = value.toString() if s.isEmpty(): return False item = index.internalPointer() if item.setData(index.column(), value): self.dataChanged.emit(index, index) return True return False # functions not defined in QAbstractItemModel def addRepo(self, uroot, row=-1, parent=QModelIndex()): if not parent.isValid(): parent = self._indexFromItem(self.allrepos) rgi = parent.internalPointer() if row < 0: row = rgi.childCount() # make sure all paths are properly normalized root = os.path.normpath(hglib.fromunicode(uroot)) # Check whether the repo that we are adding is a subrepo knownitem = self.getRepoItem(root, lookForSubrepos=True) itemIsSubrepo = isinstance(knownitem, (repotreeitem.StandaloneSubrepoItem, repotreeitem.SubrepoItem)) self.beginInsertRows(parent, row, row) if itemIsSubrepo: ri = repotreeitem.StandaloneSubrepoItem(root) else: ri = repotreeitem.RepoItem(root) rgi.insertChild(row, ri) self.endInsertRows() return self._indexFromItem(ri) # TODO: merge getRepoItem() to indexFromRepoRoot() def getRepoItem(self, reporoot, lookForSubrepos=False): reporoot = os.path.normcase(reporoot) items = getRepoItemList(self.rootItem, standalone=not lookForSubrepos) for e in items: if os.path.normcase(e.rootpath()) == reporoot: return e def indexFromRepoRoot(self, uroot, column=0, standalone=False): item = self.getRepoItem(hglib.fromunicode(uroot), lookForSubrepos=not standalone) return self._indexFromItem(item, column) def isKnownRepoRoot(self, uroot, standalone=False): return self.indexFromRepoRoot(uroot, standalone=standalone).isValid() def indexesOfRepoItems(self, column=0, standalone=False): return [self._indexFromItem(e, column) for e in getRepoItemList(self.rootItem, standalone)] def _indexFromItem(self, item, column=0): if item: return self.createIndex(item.row(), column, item) else: return QModelIndex() def repoRoot(self, index): item = index.internalPointer() if not isinstance(item, repotreeitem.RepoItem): return return hglib.tounicode(item.rootpath()) def addGroup(self, name): ri = self.rootItem cc = ri.childCount() self.beginInsertRows(QModelIndex(), cc, cc + 1) ri.appendChild(repotreeitem.RepoGroupItem(name, ri)) self.endInsertRows() def write(self, fn): f = QFile(fn) f.open(QIODevice.WriteOnly) writeXml(f, self.rootItem, reporegistryXmlElementName) f.close() def _emitItemDataChanged(self, item): self.dataChanged.emit(self._indexFromItem(item, 0), self._indexFromItem(item, self.columnCount())) def setActiveRepo(self, index): """Highlight the specified item as active""" newitem = index.internalPointer() if newitem is self._activeRepoItem: return previtem = self._activeRepoItem self._activeRepoItem = newitem for it in [previtem, newitem]: if it: self._emitItemDataChanged(it) def activeRepoIndex(self, column=0): return self._indexFromItem(self._activeRepoItem, column) def loadSubrepos(self, index): """Scan subrepos of the repo; returns list of invalid paths""" item = index.internalPointer() if (not isinstance(item, repotreeitem.RepoItem) or isinstance(item, repotreeitem.AlienSubrepoItem)): return [] self.removeRows(0, item.childCount(), index) # XXX dirty hack to know childCount _before_ insertion; should be # fixed later when you refactor appendSubrepos(). tmpitem = item.__class__(item.rootpath()) invalidpaths = tmpitem.appendSubrepos() if tmpitem.childCount() > 0: self.beginInsertRows(index, 0, tmpitem.childCount() - 1) for e in tmpitem.childs: item.appendChild(e) self.endInsertRows() if item._valid != tmpitem._valid: item._valid = tmpitem._valid self._emitItemDataChanged(item) return map(hglib.tounicode, invalidpaths) def updateCommonPaths(self, showShortPaths=None): if not showShortPaths is None: self.showShortPaths = showShortPaths for grp in self.rootItem.childs: if isinstance(grp, repotreeitem.RepoGroupItem): if self.showShortPaths: grp.updateCommonPath() else: grp.updateCommonPath('') @pyqtSlot(unicode) def _updateShortName(self, uroot): repo = self._repomanager.repoAgent(uroot).rawRepo() it = self.getRepoItem(hglib.fromunicode(uroot)) if it: it.setShortName(repo.shortname) self._emitItemDataChanged(it) @pyqtSlot(unicode) def _updateBaseNode(self, uroot): repo = self._repomanager.repoAgent(uroot).rawRepo() it = self.getRepoItem(hglib.fromunicode(uroot)) if it: it.setBaseNode(repo[0].node()) @pyqtSlot(unicode) def _updateItem(self, uroot): self._updateShortName(uroot) self._updateBaseNode(uroot) def sortchilds(self, childs, keyfunc): self.layoutAboutToBeChanged.emit() childs.sort(key=keyfunc) self.layoutChanged.emit() tortoisehg-2.10/tortoisehg/hgqt/clone.py0000644000076400007640000004651512231647662017521 0ustar stevesteve# clone.py - Clone dialog for TortoiseHg # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import ui, cmdutil, commands from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, qtlib class CloneDialog(QDialog): cmdfinished = pyqtSignal(int) clonedRepository = pyqtSignal(QString, QString) def __init__(self, args=None, opts={}, parent=None): super(CloneDialog, self).__init__(parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self.ui = ui.ui() self.ret = None dest = src = cwd = os.getcwd() if args: if len(args) > 1: src = args[0] dest = args[1] else: src = args[0] udest = hglib.tounicode(dest) usrc = hglib.tounicode(src) ucwd = hglib.tounicode(cwd) self.prev_dest = None # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid grid = QGridLayout() grid.setSpacing(6) box.addLayout(grid) ### source combo and button self.src_combo = QComboBox() self.src_combo.setEditable(True) self.src_combo.setMinimumWidth(310) self.src_btn = QPushButton(_('Browse...')) self.src_btn.setAutoDefault(False) self.src_btn.clicked.connect(self.browse_src) grid.addWidget(QLabel(_('Source:')), 0, 0) grid.addWidget(self.src_combo, 0, 1) grid.addWidget(self.src_btn, 0, 2) ### destination combo and button self.dest_combo = QComboBox() self.dest_combo.setEditable(True) self.dest_combo.setMinimumWidth(310) self.dest_btn = QPushButton(_('Browse...')) self.dest_btn.setAutoDefault(False) self.dest_btn.clicked.connect(self.browse_dest) grid.addWidget(QLabel(_('Destination:')), 1, 0) grid.addWidget(self.dest_combo, 1, 1) grid.addWidget(self.dest_btn, 1, 2) for combo in (self.src_combo, self.dest_combo): qtlib.allowCaseChangingInput(combo) s = QSettings() self.shist = s.value('clone/source').toStringList() for path in self.shist: if path: self.src_combo.addItem(path) self.src_combo.setCurrentIndex(-1) self.src_combo.setEditText(usrc) self.dhist = s.value('clone/dest').toStringList() for path in self.dhist: if path: self.dest_combo.addItem(path) self.dest_combo.setCurrentIndex(-1) self.dest_combo.setEditText(udest) ### options expander = qtlib.ExpanderLabel(_('Options'), False) expander.expanded.connect(self.show_options) grid.addWidget(expander, 2, 0, Qt.AlignLeft | Qt.AlignTop) optbox = QVBoxLayout() optbox.setSpacing(6) grid.addLayout(optbox, 2, 1, 1, 2) def chktext(chklabel, btnlabel=None, btnslot=None, stretch=None): hbox = QHBoxLayout() hbox.setSpacing(0) optbox.addLayout(hbox) chk = QCheckBox(chklabel) text = QLineEdit(enabled=False) chk.toggled.connect(text.setEnabled) chk.toggled.connect(text.setFocus) hbox.addWidget(chk) hbox.addWidget(text) if stretch is not None: hbox.addStretch(stretch) if btnlabel: btn = QPushButton(btnlabel) btn.setEnabled(False) btn.setAutoDefault(False) btn.clicked.connect(btnslot) chk.toggled.connect(btn.setEnabled) hbox.addSpacing(6) hbox.addWidget(btn) return chk, text, btn else: return chk, text self.rev_chk, self.rev_text = chktext(_('Clone to revision:'), stretch=40) self.noupdate_chk = QCheckBox(_('Do not update the new working directory')) self.pproto_chk = QCheckBox(_('Use pull protocol to copy metadata')) self.uncomp_chk = QCheckBox(_('Use uncompressed transfer')) optbox.addWidget(self.noupdate_chk) optbox.addWidget(self.pproto_chk) optbox.addWidget(self.uncomp_chk) self.qclone_chk, self.qclone_txt, self.qclone_btn = \ chktext(_('Include patch queue'), btnlabel=_('Browse...'), btnslot=self.onBrowseQclone) self.proxy_chk = QCheckBox(_('Use proxy server')) optbox.addWidget(self.proxy_chk) useproxy = bool(self.ui.config('http_proxy', 'host')) self.proxy_chk.setEnabled(useproxy) self.proxy_chk.setChecked(useproxy) self.insecure_chk = QCheckBox(_('Do not verify host certificate')) optbox.addWidget(self.insecure_chk) self.insecure_chk.setEnabled(False) self.remote_chk, self.remote_text = chktext(_('Remote command:')) # allow to specify start revision for p4 & svn repos. self.startrev_chk, self.startrev_text = chktext(_('Start revision:'), stretch=40) self.hgcmd_lbl = QLabel(_('Hg command:')) self.hgcmd_lbl.setAlignment(Qt.AlignRight) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) grid.addWidget(self.hgcmd_lbl, 3, 0) grid.addWidget(self.hgcmd_txt, 3, 1) self.hgcmd_txt.setMinimumWidth(400) ## command widget self.cmd = cmdui.Widget(True, True, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandFinished.connect(self.cmdfinished) self.cmd.commandCanceling.connect(self.command_canceling) box.addWidget(self.cmd) ## bottom buttons buttons = QDialogButtonBox() self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel) self.cancel_btn.clicked.connect(self.cmd.cancel) self.close_btn = buttons.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.onCloseClicked) self.close_btn.setAutoDefault(False) self.clone_btn = buttons.addButton(_('&Clone'), QDialogButtonBox.ActionRole) self.clone_btn.clicked.connect(self.clone) self.detail_btn = buttons.addButton(_('Detail'), QDialogButtonBox.ResetRole) self.detail_btn.setAutoDefault(False) self.detail_btn.setCheckable(True) self.detail_btn.toggled.connect(self.detail_toggled) box.addWidget(buttons) # dialog setting self.setLayout(box) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.setWindowTitle(_('Clone - %s') % ucwd) self.setWindowIcon(qtlib.geticon('hg-clone')) # connect extra signals self.src_combo.editTextChanged.connect(self.composeCommand) self.src_combo.editTextChanged.connect(self.onUrlHttps) self.src_combo.editTextChanged.connect(self.onResetDefault) self.src_combo.currentIndexChanged.connect(self.onResetDefault) self.dest_combo.editTextChanged.connect(self.composeCommand) self.rev_chk.toggled.connect(self.composeCommand) self.rev_text.textChanged.connect(self.composeCommand) self.noupdate_chk.toggled.connect(self.composeCommand) self.pproto_chk.toggled.connect(self.composeCommand) self.uncomp_chk.toggled.connect(self.composeCommand) self.qclone_chk.toggled.connect(self.composeCommand) self.qclone_txt.textChanged.connect(self.composeCommand) self.proxy_chk.toggled.connect(self.composeCommand) self.insecure_chk.toggled.connect(self.composeCommand) self.remote_chk.toggled.connect(self.composeCommand) self.remote_text.textChanged.connect(self.composeCommand) self.startrev_chk.toggled.connect(self.composeCommand) # prepare to show self.cmd.setHidden(True) self.cancel_btn.setHidden(True) self.detail_btn.setHidden(True) self.show_options(False) rev = opts.get('rev') if rev: self.rev_chk.setChecked(True) self.rev_text.setText(hglib.tounicode(rev)) self.noupdate_chk.setChecked(bool(opts.get('noupdate'))) self.pproto_chk.setChecked(bool(opts.get('pull'))) self.uncomp_chk.setChecked(bool(opts.get('uncompressed'))) self.src_combo.setFocus() self.src_combo.lineEdit().selectAll() self.composeCommand() def setSource(self, url): assert not self.isRunning() self.src_combo.setEditText(url) def setDestination(self, url): assert not self.isRunning() self.dest_combo.setEditText(url) def isRunning(self): return self.cmd.core.running() ### Private Methods ### def getSrc(self): return hglib.fromunicode(self.src_combo.currentText()).strip() def getDest(self): return hglib.fromunicode(self.dest_combo.currentText()).strip() def show_options(self, visible): self.rev_chk.setVisible(visible) self.rev_text.setVisible(visible) self.noupdate_chk.setVisible(visible) self.pproto_chk.setVisible(visible) self.uncomp_chk.setVisible(visible) self.proxy_chk.setVisible(visible) self.insecure_chk.setVisible(visible) self.qclone_chk.setVisible(visible) self.qclone_txt.setVisible(visible) self.qclone_btn.setVisible(visible) self.remote_chk.setVisible(visible) self.remote_text.setVisible(visible) self.startrev_chk.setVisible(visible and self.startrev_available()) self.startrev_text.setVisible(visible and self.startrev_available()) def composeCommand(self): remotecmd = hglib.fromunicode(self.remote_text.text().trimmed()) rev = hglib.fromunicode(self.rev_text.text().trimmed()) startrev = hglib.fromunicode(self.startrev_text.text().trimmed()) if self.qclone_chk.isChecked(): cmdline = ['qclone'] qclonedir = hglib.fromunicode(self.qclone_txt.text().trimmed()) if qclonedir: cmdline += ['--patches', qclonedir] else: cmdline = ['clone'] if self.noupdate_chk.isChecked(): cmdline.append('--noupdate') if self.uncomp_chk.isChecked(): cmdline.append('--uncompressed') if self.pproto_chk.isChecked(): cmdline.append('--pull') if self.ui.config('http_proxy', 'host'): if not self.proxy_chk.isChecked(): cmdline += ['--config', 'http_proxy.host='] if self.remote_chk.isChecked() and remotecmd: cmdline.append('--remotecmd') cmdline.append(remotecmd) if self.rev_chk.isChecked() and rev: cmdline.append('--rev') cmdline.append(rev) if self.startrev_chk.isChecked() and startrev: cmdline.append('--startrev') cmdline.append(startrev) cmdline.append('--verbose') src = self.getSrc() dest = self.getDest() if self.insecure_chk.isChecked() and src.startswith('https://'): cmdline.append('--insecure') cmdline.append('--') cmdline.append(src) if dest: cmdline.append(dest) self.hgcmd_txt.setText(hglib.tounicode(' '.join(['hg'] + cmdline))) return cmdline def startrev_available(self): entry = cmdutil.findcmd('clone', commands.table)[1] longopts = set(e[1] for e in entry[1]) return 'startrev' in longopts def clone(self): if self.cmd.core.running(): return # prepare user input srcQ = self.src_combo.currentText().trimmed() src = hglib.fromunicode(srcQ) destQ = self.dest_combo.currentText().trimmed() dest = hglib.fromunicode(destQ) if not dest: dest = os.path.basename(src) destQ = QString(hglib.tounicode(dest)) if not dest.startswith('ssh://'): if not os.path.exists(dest): try: os.mkdir(dest) except EnvironmentError: qtlib.ErrorMsgBox(_('TortoiseHg Clone'), _('Error creating destination folder'), _('Please specify a different path.')) return False self.prev_dest = None if srcQ: l = list(self.shist) if srcQ in l: l.remove(srcQ) l.insert(0, srcQ) self.shist = l[:10] if destQ: l = list(self.dhist) if destQ in l: l.remove(destQ) l.insert(0, destQ) self.dhist = l[:10] s = QSettings() s.setValue('clone/source', self.shist) s.setValue('clone/dest', self.dhist) # verify input if src == '': qtlib.ErrorMsgBox(_('TortoiseHg Clone'), _('Source path is empty'), _('Please enter a valid source path.')) self.src_combo.setFocus() return False if src == dest: qtlib.ErrorMsgBox(_('TortoiseHg Clone'), _('Source and destination are the same'), _('Please specify different paths.')) return False if dest == os.getcwd(): if os.listdir(dest): # cur dir has files, specify no dest, let hg take # basename dest = '' else: dest = '.' else: abs = os.path.abspath(dest) dirabs = os.path.dirname(abs) if dirabs == src: dest = os.path.join(os.path.dirname(dirabs), dest) # prepare command line self.src_combo.setEditText(hglib.tounicode(src)) self.dest_combo.setEditText(hglib.tounicode(dest)) cmdline = self.composeCommand() # do not make the same clone twice (see #514) if dest == self.prev_dest and os.path.exists(dest) and self.ret == 0: qtlib.ErrorMsgBox(_('TortoiseHg Clone'), _('Please enter a new destination path.')) self.dest_combo.setFocus() return self.prev_dest = dest # start cloning self.cmd.run(cmdline, useproc=src.startswith('p4://')) ### Signal Handlers ### def detail_toggled(self, checked): self.cmd.setShowOutput(checked) def browse_src(self): FD = QFileDialog caption = _("Select source repository") path = FD.getExistingDirectory(self, caption, \ self.src_combo.currentText(), QFileDialog.ShowDirsOnly) if path: self.src_combo.setEditText(QDir.toNativeSeparators(path)) self.dest_combo.setFocus() self.composeCommand() def browse_dest(self): FD = QFileDialog caption = _("Select destination repository") path = FD.getExistingDirectory(self, caption, \ self.dest_combo.currentText(), QFileDialog.ShowDirsOnly) if path: self.dest_combo.setEditText(QDir.toNativeSeparators(path)) self.dest_combo.setFocus() self.composeCommand() def onBrowseQclone(self): FD = QFileDialog caption = _("Select patch folder") upatchroot = os.path.join(unicode(self.src_combo.currentText()), '.hg') upath = FD.getExistingDirectory(self, caption, upatchroot, QFileDialog.ShowDirsOnly) if upath: self.qclone_txt.setText(QDir.toNativeSeparators(upath)) self.qclone_txt.setFocus() self.composeCommand() @pyqtSlot() def onResetDefault(self): self.clone_btn.setDefault(True) def command_started(self): self.cmd.setShown(True) self.clone_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setEnabled(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) self.setChoicesActive(False) def command_finished(self, ret): self.ret = ret if ret != 0 or self.cmd.outputShown(): self.detail_btn.setChecked(True) self.clone_btn.setShown(True) self.close_btn.setShown(True) self.close_btn.setDefault(True) self.close_btn.setFocus() self.cancel_btn.setHidden(True) self.setChoicesActive(True) if not ret: # Let the workbench know that a repository has been successfully # cloned self.clonedRepository.emit(self.dest_combo.currentText(), self.src_combo.currentText()) if ret == 0 and not self.cmd.outputShown(): self.accept() def onCloseClicked(self): if self.ret == 0: self.accept() else: self.reject() def onUrlHttps(self): self.insecure_chk.setEnabled(self.getSrc().startswith('https://')) self.composeCommand() def command_canceling(self): self.cancel_btn.setDisabled(True) def setChoicesActive(self, mode): if mode: self.src_combo.setEnabled(True) self.src_btn.setEnabled(True) self.dest_combo.setEnabled(True) self.dest_btn.setEnabled(True) self.rev_chk.setEnabled(True) self.rev_text.setEnabled(self.rev_chk.isChecked()) self.noupdate_chk.setEnabled(True) self.pproto_chk.setEnabled(True) self.uncomp_chk.setEnabled(True) self.qclone_chk.setEnabled(True) self.qclone_txt.setEnabled(self.qclone_chk.isChecked()) self.qclone_btn.setEnabled(self.qclone_chk.isChecked()) self.proxy_chk.setEnabled(True) self.insecure_chk.setEnabled(True) self.remote_chk.setEnabled(True) self.remote_text.setEnabled(self.remote_chk.isChecked()) self.startrev_chk.setEnabled(True) self.startrev_text.setEnabled(self.startrev_chk.isChecked()) else: self.src_combo.setDisabled(True) self.src_btn.setDisabled(True) self.dest_combo.setDisabled(True) self.dest_btn.setDisabled(True) self.rev_chk.setDisabled(True) self.rev_text.setDisabled(True) self.noupdate_chk.setDisabled(True) self.pproto_chk.setDisabled(True) self.uncomp_chk.setDisabled(True) self.qclone_chk.setDisabled(True) self.qclone_txt.setDisabled(True) self.qclone_btn.setDisabled(True) self.proxy_chk.setDisabled(True) self.insecure_chk.setDisabled(True) self.remote_chk.setDisabled(True) self.remote_text.setDisabled(True) self.startrev_chk.setDisabled(True) self.startrev_text.setDisabled(True) def accept(self): if self.cmd.core.running(): return QDialog.accept(self) def reject(self): if self.cmd.core.running(): self.cmd.cancel() return QDialog.reject(self) tortoisehg-2.10/tortoisehg/hgqt/quickop.py0000644000076400007640000002473512231647662020074 0ustar stevesteve# quickop.py - TortoiseHg's dialog for quick dirstate operations # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import sys from mercurial import util from tortoisehg.util import hglib, shlib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, status, cmdcore, cmdui, lfprompt from PyQt4.QtCore import * from PyQt4.QtGui import * LABELS = { 'add': (_('Checkmark files to add'), _('Add')), 'forget': (_('Checkmark files to forget'), _('Forget')), 'revert': (_('Checkmark files to revert'), _('Revert')), 'remove': (_('Checkmark files to remove'), _('Remove')),} ICONS = { 'add': 'fileadd', 'forget': 'hg-remove', 'revert': 'hg-revert', 'remove': 'hg-remove',} class QuickOpDialog(QDialog): """ Dialog for performing quick dirstate operations """ def __init__(self, repoagent, command, pats, parent): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.pats = pats self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._cmddialog = cmdui.CmdSessionDialog(self) repo = repoagent.rawRepo() # Handle rm alias if command == 'rm': command = 'remove' self.command = command self.setWindowTitle(_('%s - hg %s') % (repo.displayname, command)) self.setWindowIcon(qtlib.geticon(ICONS[command])) layout = QVBoxLayout() layout.setMargin(0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setContentsMargins(5, 5, 5, 0) layout.addLayout(toplayout) hbox = QHBoxLayout() lbl = QLabel(LABELS[command][0]) slbl = QLabel() hbox.addWidget(lbl) hbox.addStretch(1) hbox.addWidget(slbl) self.status_label = slbl toplayout.addLayout(hbox) types = { 'add' : 'I?', 'forget' : 'MAR!C', 'revert' : 'MAR!', 'remove' : 'MAR!CI?', } filetypes = types[self.command] checktypes = { 'add' : '?', 'forget' : '', 'revert' : 'MAR!', 'remove' : '', } defcheck = checktypes[self.command] opts = {} for s, val in status.statusTypes.iteritems(): opts[val.name] = s in filetypes opts['checkall'] = True # pre-check all matching files stwidget = status.StatusWidget(repoagent, pats, opts, self, defcheck=defcheck) toplayout.addWidget(stwidget, 1) hbox = QHBoxLayout() if self.command == 'revert': ## no backup checkbox chk = QCheckBox(_('Do not save backup files (*.orig)')) elif self.command == 'remove': ## force checkbox chk = QCheckBox(_('Force removal of modified files (--force)')) else: chk = None if chk: self.chk = chk hbox.addWidget(chk) self.statusbar = cmdui.ThgStatusBar(self) stwidget.showMessage.connect(self.statusbar.showMessage) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Close) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) bb.button(BB.Ok).setText(LABELS[command][1]) hbox.addStretch() hbox.addWidget(bb) toplayout.addLayout(hbox) self.bb = bb if self.command == 'add': if 'largefiles' in self.repo.extensions(): self.addLfilesButton = QPushButton(_('Add &Largefiles')) else: self.addLfilesButton = None if self.addLfilesButton: self.addLfilesButton.clicked.connect(self.addLfiles) bb.addButton(self.addLfilesButton, BB.ActionRole) layout.addWidget(self.statusbar) s = QSettings() stwidget.loadSettings(s, 'quickop') self.restoreGeometry(s.value('quickop/geom').toByteArray()) if hasattr(self, 'chk'): if self.command == 'revert': self.chk.setChecked(s.value('quickop/nobackup', True).toBool()) elif self.command == 'remove': self.chk.setChecked( s.value('quickop/forceremove', False).toBool()) self.stwidget = stwidget self.stwidget.refreshWctx() QShortcut(QKeySequence('Ctrl+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) qtlib.newshortcutsforstdkey(QKeySequence.Refresh, self, self.stwidget.refreshWctx) QShortcut(QKeySequence('Escape'), self, self.reject) @property def repo(self): return self._repoagent.rawRepo() def _runCommand(self, files, lfiles, opts): cmdlines = [] if files: cmdlines.append(hglib.buildcmdargs(self.command, *files, **opts)) if lfiles: assert self.command == 'add' lopts = opts.copy() lopts['large'] = True cmdlines.append(hglib.buildcmdargs(self.command, *lfiles, **lopts)) self.files = files + lfiles ucmdlines = [map(hglib.tounicode, xs) for xs in cmdlines] self._cmdsession = sess = self._repoagent.runCommandSequence(ucmdlines, self) sess.commandFinished.connect(self.commandFinished) sess.progressReceived.connect(self.statusbar.progress) self._cmddialog.setSession(sess) self.bb.button(QDialogButtonBox.Ok).setEnabled(False) def commandFinished(self, ret): self.bb.button(QDialogButtonBox.Ok).setEnabled(True) if ret == 0: shlib.shell_notify(self.files) self.reject() else: self._cmddialog.show() def accept(self): cmdopts = {} if hasattr(self, 'chk'): if self.command == 'revert': cmdopts['no_backup'] = self.chk.isChecked() elif self.command == 'remove': cmdopts['force'] = self.chk.isChecked() files = self.stwidget.getChecked() if not files: qtlib.WarningMsgBox(_('No files selected'), _('No operation to perform'), parent=self) return self.repo.bfstatus = True self.repo.lfstatus = True repostate = self.repo.status() self.repo.bfstatus = False self.repo.lfstatus = False if self.command == 'remove': if not self.chk.isChecked(): modified = repostate[0] selmodified = [] for wfile in files: if wfile in modified: selmodified.append(wfile) if selmodified: prompt = qtlib.CustomPrompt( _('Confirm Remove'), _('You have selected one or more files that have been ' 'modified. By default, these files will not be ' 'removed. What would you like to do?'), self, (_('Remove &Unmodified Files'), _('Remove &All Selected Files'), _('Cancel')), 0, 2, selmodified) ret = prompt.run() if ret == 1: cmdopts['force'] = True elif ret == 2: return unknown, ignored = repostate[4:6] for wfile in files: if wfile in unknown or wfile in ignored: try: util.unlink(wfile) except EnvironmentError: pass files.remove(wfile) elif self.command == 'add': if 'largefiles' in self.repo.extensions(): self.addWithPrompt(files) return if files: self._runCommand(files, [], cmdopts) else: self.reject() def reject(self): if not self._cmdsession.isFinished(): self._cmdsession.abort() elif not self.stwidget.canExit(): return else: s = QSettings() self.stwidget.saveSettings(s, 'quickop') s.setValue('quickop/geom', self.saveGeometry()) if hasattr(self, 'chk'): if self.command == 'revert': s.setValue('quickop/nobackup', self.chk.isChecked()) elif self.command == 'remove': s.setValue('quickop/forceremove', self.chk.isChecked()) QDialog.reject(self) def addLfiles(self): files = self.stwidget.getChecked() if not files: qtlib.WarningMsgBox(_('No files selected'), _('No operation to perform'), parent=self) return self._runCommand([], files, {}) def addWithPrompt(self, files): result = lfprompt.promptForLfiles(self, self.repo.ui, self.repo, files) if not result: return files, lfiles = result self._runCommand(files, lfiles, {}) class HeadlessQuickop(QObject): def __init__(self, repoagent, cmdline): QObject.__init__(self) self.files = cmdline[1:] self._cmddialog = cmdui.CmdSessionDialog() sess = repoagent.runCommand(map(hglib.tounicode, cmdline)) sess.commandFinished.connect(self.commandFinished) self._cmddialog.setSession(sess) def commandFinished(self, ret): if ret == 0: shlib.shell_notify(self.files) sys.exit(0) else: self._cmddialog.show() # dummy methods to act as QWidget (see run.qtrun) def show(self): pass def raise_(self): pass def run(ui, repoagent, *pats, **opts): repo = repoagent.rawRepo() pats = hglib.canonpaths(pats) command = opts['alias'] imm = repo.ui.config('tortoisehg', 'immediate', '') if opts.get('headless') or command in imm.lower(): cmdline = [command] + pats return HeadlessQuickop(repoagent, cmdline) else: return QuickOpDialog(repoagent, command, pats, None) tortoisehg-2.10/tortoisehg/hgqt/wctxactions.py0000644000076400007640000004007412231647662020761 0ustar stevesteve# wctxactions.py - menu and responses for working copy files # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import util, error, merge, commands, extensions from tortoisehg.hgqt import qtlib, htmlui, visdiff, lfprompt, customtools from tortoisehg.hgqt import filedialogs from tortoisehg.util import hglib, shlib from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import Qt, QObject, QDir, pyqtSignal, pyqtSlot from PyQt4.QtGui import * class WctxActions(QObject): 'container class for working context actions' refreshNeeded = pyqtSignal() runCustomCommandRequested = pyqtSignal(str, list) def __init__(self, repoagent, parent, checkable=True): super(WctxActions, self).__init__(parent) self.menu = QMenu(parent) self._repoagent = repoagent self._filedialogs = qtlib.DialogKeeper(WctxActions._createFileDialog, parent=self) allactions = [] def make(text, func, types, icon=None, keys=None, slot=self.runAction): action = QAction(text, parent) action._filetypes = types action._runfunc = func if icon: action.setIcon(qtlib.geticon(icon)) if keys: action.setShortcut(QKeySequence(keys)) action.triggered.connect(slot) parent.addAction(action) allactions.append(action) make(_('&Diff to Parent'), vdiff, frozenset('MAR!'), 'visualdiff', 'CTRL+D') make(_('&Copy Patch'), copyPatch, frozenset('MAR!'), 'copy-patch') make(_('&Edit'), edit, frozenset('MACI?'), 'edit-file', 'SHIFT+CTRL+E') make(_('&Open'), openfile, frozenset('MACI?'), '', 'SHIFT+CTRL+L') allactions.append(None) make(_('Open S&ubrepository'), opensubrepo, frozenset('S'), 'thg-repository-open') make(_('E&xplore Subrepository'), explore, frozenset('S'), 'system-file-manager') make(_('Open &Terminal'), terminal, frozenset('S'), 'utilities-terminal') allactions.append(None) make(_('Copy &Path'), copyPath, frozenset('MARC?!IS'), '') make(_('&View Missing'), viewmissing, frozenset('R!')) allactions.append(None) make(_('&Revert...'), revert, frozenset('SMAR!'), 'hg-revert') make(_('&Add'), add, frozenset('R'), 'fileadd') allactions.append(None) make(_('File &History'), None, frozenset('MARC!'), 'hg-log', slot=self.log) allactions.append(None) make(_('&Forget'), forget, frozenset('MC!'), 'filedelete') make(_('&Add'), add, frozenset('I?'), 'fileadd') if 'largefiles' in self.repo.extensions(): make(_('Add &Largefiles...'), addlf, frozenset('I?')) make(_('De&tect Renames...'), guessRename, frozenset('A?!'), 'detect_rename', slot=self.runDialogAction) make(_('&Ignore...'), ignore, frozenset('?'), 'ignore', slot=self.runDialogAction) make(_('Re&move Versioned'), remove, frozenset('C'), 'remove') make(_('&Delete Unversioned...'), delete, frozenset('?I'), 'hg-purge') allactions.append(None) make(_('&Mark Unresolved'), unmark, frozenset('r')) make(_('&Mark Resolved'), mark, frozenset('u')) if checkable: # no &-shortcut because check/uncheck can be done by space key allactions.append(None) make(_('Check'), check, frozenset('MARC?!IS'), '') make(_('Uncheck'), uncheck, frozenset('MARC?!IS'), '') self.allactions = allactions @property def repo(self): return self._repoagent.rawRepo() def updateActionSensitivity(self, selrows): 'Enable/Disable permanent actions based on current selection' self.selrows = selrows alltypes = set() for types, wfile in selrows: alltypes |= types for action in self.allactions: if action is not None: action.setEnabled(bool(action._filetypes & alltypes)) def makeMenu(self, selrows): self.selrows = selrows repo, menu = self.repo, self.menu alltypes = set() for types, wfile in selrows: alltypes |= types menu.clear() addedActions = False for action in self.allactions: if action is None: if addedActions: menu.addSeparator() addedActions = False elif action._filetypes & alltypes: menu.addAction(action) addedActions = True def make(text, func, types=None, icon=None, inmenu=None, slot=self.runAction): if not types & alltypes: return if inmenu is None: inmenu = menu action = inmenu.addAction(text) action._filetypes = types action._runfunc = func if icon: action.setIcon(qtlib.geticon(icon)) if func is not None: action.triggered.connect(slot) return action if len(repo.parents()) > 1: make(_('View O&ther'), viewother, frozenset('MA')) if len(selrows) == 1: menu.addSeparator() make(_('&Copy...'), copy, frozenset('MC'), 'edit-copy', slot=self.runDialogAction) make(_('Re&name...'), rename, frozenset('MC'), 'hg-rename', slot=self.runDialogAction) menu.addSeparator() customtools.addCustomToolsSubmenu(menu, repo.ui, location='workbench.commit.custom-menu', make=make, slot=self._runCustomCommandByMenu) # Add 'was renamed from' actions for unknown files t, path = selrows[0] wctx = self.repo[None] if t & frozenset('?') and wctx.deleted(): rmenu = QMenu(_('Was renamed from'), self.parent()) for d in wctx.deleted()[:15]: def mkaction(deleted): a = rmenu.addAction(hglib.tounicode(deleted)) a.triggered.connect(lambda: renamefromto(repo, deleted, path)) mkaction(d) menu.addSeparator() menu.addMenu(rmenu) # Add restart merge actions for resolved files if alltypes & frozenset('u'): f = make(_('Restart Mer&ge'), resolve, frozenset('u')) files = [f for t, f in selrows if 'u' in t] rmenu = QMenu(_('Restart Merge &with'), self.parent()) for tool in hglib.mergetools(repo.ui): def mkaction(rtool): a = rmenu.addAction(hglib.tounicode(rtool)) a.triggered.connect(lambda: resolve_with(rtool, repo, files)) mkaction(tool) menu.addSeparator() menu.addMenu(rmenu) return menu def _filesForAction(self, action): return [wfile for t, wfile in self.selrows if t & action._filetypes] @pyqtSlot(QAction) def _runCustomCommandByMenu(self, action): files = self._filesForAction(action) self.runCustomCommandRequested.emit( str(action.data().toString()), files) def runAction(self): 'run wrapper for direct action methods' repo, action, parent = self.repo, self.sender(), self.parent() func = action._runfunc files = self._filesForAction(action) hu = htmlui.htmlui() name = hglib.tounicode(func.__name__.title()) notify = False cwd = os.getcwd() try: os.chdir(repo.root) try: # All operations should quietly succeed. Any error should # result in a message box notify = func(parent, hu, repo, files) o, e = hu.getdata() if e: QMessageBox.warning(parent, name + _(' errors'), hglib.tounicode(e)) elif o: QMessageBox.information(parent, name + _(' output'), hglib.tounicode(o)) elif notify: wfiles = [repo.wjoin(x) for x in files] shlib.shell_notify(wfiles) except (IOError, OSError), e: err = hglib.tounicode(str(e)) QMessageBox.critical(parent, name + _(' Aborted'), err) except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) QMessageBox.critical(parent, name + _(' Aborted'), err) except (error.LookupError), e: err = hglib.tounicode(str(e)) QMessageBox.critical(parent, name + _(' Aborted'), err) finally: os.chdir(cwd) if notify: self.refreshNeeded.emit() #@pyqtSlot() def runDialogAction(self): 'run wrapper for modal dialog action methods' repo, action, parent = self.repo, self.sender(), self.parent() func = action._runfunc files = self._filesForAction(action) notify = func(parent, self._repoagent, files) if notify: wfiles = [repo.wjoin(x) for x in files] shlib.shell_notify(wfiles) self.refreshNeeded.emit() #@pyqtSlot() def log(self): for path in self._filesForAction(self.sender()): self._filedialogs.open(path) def _createFileDialog(self, path): return filedialogs.FileLogDialog(self._repoagent, path) def renamefromto(repo, deleted, unknown): repo[None].copy(deleted, unknown) repo[None].forget([deleted]) # !->R def copyPatch(parent, ui, repo, files): ui.pushbuffer() try: commands.diff(ui, repo, *files) except Exception, e: ui.popbuffer() if 'THGDEBUG' in os.environ: import traceback traceback.print_exc() return output = ui.popbuffer() QApplication.clipboard().setText(hglib.tounicode(output)) def copyPath(parent, ui, repo, files): clip = QApplication.clipboard() absfiles = [hglib.fromunicode(QDir.toNativeSeparators(repo.wjoin(fname))) for fname in files] clip.setText(hglib.tounicode(os.linesep.join(absfiles))) def vdiff(parent, ui, repo, files): dlg = visdiff.visualdiff(ui, repo, files, {}) if dlg: dlg.exec_() def edit(parent, ui, repo, files, lineno=None, search=None): qtlib.editfiles(repo, files, lineno, search, parent) def openfile(parent, ui, repo, files): qtlib.openfiles(repo, files, parent) def opensubrepo(parent, ui, repo, files): for filename in files: path = os.path.join(repo.root, filename) if os.path.isdir(path): parent.linkActivated.emit(u'repo:' + hglib.tounicode(path)) else: QMessageBox.warning(parent, _("Cannot open subrepository"), _("The selected subrepository does not exist on the working directory")) def explore(parent, ui, repo, files): qtlib.openfiles(repo, files, parent) def terminal(parent, ui, repo, files): for filename in files: root = repo.wjoin(filename) if os.path.isdir(root): qtlib.openshell(root, filename, repo.ui) def viewmissing(parent, ui, repo, files): base, _ = visdiff.snapshot(repo, files, repo['.']) edit(parent, ui, repo, [os.path.join(base, f) for f in files]) def viewother(parent, ui, repo, files): wctx = repo[None] assert bool(wctx.p2()) base, _ = visdiff.snapshot(repo, files, wctx.p2()) edit(parent, ui, repo, [os.path.join(base, f) for f in files]) def revert(parent, ui, repo, files): revertopts = {'date': None, 'rev': '.', 'all': False} if len(repo.parents()) > 1: res = qtlib.CustomPrompt( _('Uncommited merge - please select a parent revision'), _('Revert files to local or other parent?'), parent, (_('&Local'), _('&Other'), _('Cancel')), 0, 2, files).run() if res == 0: revertopts['rev'] = repo[None].p1().rev() elif res == 1: revertopts['rev'] = repo[None].p2().rev() else: return False commands.revert(ui, repo, *files, **revertopts) else: wctx = repo[None] if [file for file in files if file in wctx.modified()]: res = qtlib.CustomPrompt( _('Confirm Revert'), _('Revert local file changes?'), parent, (_('&Revert with backup'), _('&Discard changes'), _('Cancel')), 2, 2, files).run() if res == 2: return False if res == 1: revertopts['no_backup'] = True else: res = qtlib.CustomPrompt( _('Confirm Revert'), _('Revert the following files?'), parent, (_('&Revert'), _('Cancel')), 1, 1, files).run() if res == 1: return False commands.revert(ui, repo, *files, **revertopts) return True def forget(parent, ui, repo, files): commands.forget(ui, repo, *files) return True def add(parent, ui, repo, files): if 'largefiles' in repo.extensions(): result = lfprompt.promptForLfiles(parent, ui, repo, files) if not result: return False files, lfiles = result if files: commands.add(ui, repo, normal=True, *files) if lfiles: commands.add(ui, repo, lfsize='', normal=False, large=True, *lfiles) else: commands.add(ui, repo, *files) return True def addlf(parent, ui, repo, files): commands.add(ui, repo, lfsize='', normal=None, large=True, *files) return True def guessRename(parent, repoagent, files): from tortoisehg.hgqt.guess import DetectRenameDialog dlg = DetectRenameDialog(repoagent, parent, *files) def matched(): ret[0] = True ret = [False] dlg.matchAccepted.connect(matched) dlg.finished.connect(dlg.deleteLater) dlg.exec_() return ret[0] def ignore(parent, repoagent, files): from tortoisehg.hgqt.hgignore import HgignoreDialog dlg = HgignoreDialog(repoagent, parent, *files) dlg.finished.connect(dlg.deleteLater) return dlg.exec_() == QDialog.Accepted def remove(parent, ui, repo, files): commands.remove(ui, repo, *files) return True def delete(parent, ui, repo, files): res = qtlib.CustomPrompt( _('Confirm Delete Unversioned'), _('Delete the following unversioned files?'), parent, (_('&Delete'), _('Cancel')), 1, 1, files).run() if res == 1: return False for wfile in files: os.unlink(wfile) return True def copy(parent, repoagent, files): from tortoisehg.hgqt.rename import RenameDialog assert len(files) == 1 dlg = RenameDialog(repoagent, files, parent, iscopy=True) dlg.finished.connect(dlg.deleteLater) dlg.exec_() return True def rename(parent, repoagent, files): from tortoisehg.hgqt.rename import RenameDialog assert len(files) == 1 dlg = RenameDialog(repoagent, files, parent) dlg.finished.connect(dlg.deleteLater) dlg.exec_() return True def unmark(parent, ui, repo, files): ms = merge.mergestate(repo) for wfile in files: ms.mark(wfile, 'u') ms.commit() return True def mark(parent, ui, repo, files): ms = merge.mergestate(repo) for wfile in files: ms.mark(wfile, 'r') ms.commit() return True def resolve(parent, ui, repo, files): commands.resolve(ui, repo, *files) return True def resolve_with(tool, repo, files): opts = {'tool': tool} paths = [repo.wjoin(f) for f in files] commands.resolve(repo.ui, repo, *paths, **opts) return True def check(parent, ui, repo, files): parent.tv.model().check(files, True) return True def uncheck(parent, ui, repo, files): parent.tv.model().check(files, False) return True tortoisehg-2.10/tortoisehg/hgqt/repoview.py0000644000076400007640000003634712231647662020263 0ustar stevesteve# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import htmldelegate, qtlib, repomodel from tortoisehg.hgqt.logcolumns import ColumnSelectDialog from PyQt4.QtCore import * from PyQt4.QtGui import * class HgRepoView(QTableView): revisionClicked = pyqtSignal(object) revisionAltClicked = pyqtSignal(object) revisionSelected = pyqtSignal(object) revisionActivated = pyqtSignal(object) revisionSelectionChanged = pyqtSignal(QItemSelection, QItemSelection) menuRequested = pyqtSignal(QPoint, object) showMessage = pyqtSignal(unicode) def __init__(self, repo, cfgname, colselect, parent=None): QTableView.__init__(self, parent) self.repo = repo self.current_rev = -1 self.resized = False self.cfgname = cfgname self.colselect = colselect self.setShowGrid(False) vh = self.verticalHeader() vh.hide() vh.setDefaultSectionSize(20) header = self.horizontalHeader() header.setClickable(False) # AlignBottom because RepoWidget steals top of header space for InfoBar header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignBottom) header.setHighlightSections(False) header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(self.headerMenuRequest) self.createActions() self.standardDelegate = self.itemDelegate() self.htmlDelegate = htmldelegate.HTMLDelegate(self) self.graphDelegate = GraphDelegate(self) self.setAcceptDrops(True) if PYQT_VERSION >= 0x40700: self.setDefaultDropAction(Qt.MoveAction) self.setDragEnabled(True) self.setDropIndicatorShown(True) self.setDragDropMode(QAbstractItemView.InternalMove) self.setStyle(HgRepoViewStyle(self.style())) self._paletteswitcher = qtlib.PaletteSwitcher(self) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.doubleClicked.connect(self.revActivated) self.clicked.connect(self.revClicked) def setRepo(self, repo): self.repo = repo def mousePressEvent(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return if event.button() == Qt.MidButton: self.gotoAncestor(index) return QTableView.mousePressEvent(self, event) def contextMenuEvent(self, event): self.menuRequested.emit(event.globalPos(), self.selectedRevisions()) def createActions(self): menu = QMenu(self) act = QAction(_('C&hoose Log Columns...'), self) act.triggered.connect(self.setHistoryColumns) menu.addAction(act) self.headermenu = menu @pyqtSlot(QPoint) def headerMenuRequest(self, point): self.headermenu.exec_(self.horizontalHeader().mapToGlobal(point)) def setHistoryColumns(self): dlg = ColumnSelectDialog(self.colselect[0], self.colselect[1], self.model()) if dlg.exec_() == QDialog.Accepted: self.model().updateColumns() self.resizeColumns() def setModel(self, model): QTableView.setModel(self, model) #Check if the font contains the glyph needed by the model if not QFontMetrics(self.font()).inFont(QString(u'\u2605').at(0)): model.unicodestar = False if not QFontMetrics(self.font()).inFont(QString(u'\u2327').at(0)): model.unicodexinabox = False self.selectionModel().currentRowChanged.connect(self.onRowChange) self.selectionModel().selectionChanged.connect(self.revisionSelectionChanged) self.resetDelegate() self._rev_history = [] self._rev_pos = -1 self._in_history = False model.layoutChanged.connect(self.resetDelegate) def resetBrowseHistory(self, revs, reselrev=None): graph = self.model().graph self._rev_history = [r for r in revs if r in graph.nodesdict] if reselrev is not None and reselrev in self._rev_history: self._rev_pos = self._rev_history.index(reselrev) else: self._rev_pos = -1 self.forward() def resetDelegate(self): # Model column layout has changed so we need to move # our column delegate to correct location if not self.model(): return model = self.model() for c in range(model.columnCount(QModelIndex())): if model._columns[c] in ['Description', 'Changes']: self.setItemDelegateForColumn(c, self.htmlDelegate) elif model._columns[c] == 'Graph': self.setItemDelegateForColumn(c, self.graphDelegate) else: self.setItemDelegateForColumn(c, self.standardDelegate) def resizeColumns(self): if not self.model(): return hh = self.horizontalHeader() hh.setStretchLastSection(False) self._resizeColumns() hh.setStretchLastSection(True) self.resized = True def _resizeColumns(self): # _resizeColumns misbehaves if called with last section streched for c, w in enumerate(self._columnWidthHints()): self.setColumnWidth(c, w) def _columnWidthHints(self): """Return list of recommended widths of all columns""" model = self.model() fontm = QFontMetrics(self.font()) widths = [-1 for _i in xrange(model.columnCount(QModelIndex()))] key = '%s/column_widths/%s' % (self.cfgname, str(self.repo[0])) col_widths = [int(w) for w in QSettings().value(key).toStringList()] if len(model._columns) <> len(col_widths): # If the columns and widths don't match, use the calculated # widths as they will probably be a better fit (likely because # columns were changed without updating the widths) col_widths = [] for c in range(model.columnCount(QModelIndex())): if c < len(col_widths) and col_widths[c] > 0: w = col_widths[c] else: w = model.maxWidthValueForColumn(c) if isinstance(w, int): widths[c] = w elif w is not None: w = fontm.width(hglib.tounicode(str(w)) + 'w') widths[c] = w else: w = super(HgRepoView, self).sizeHintForColumn(c) widths[c] = w return widths def revFromindex(self, index): if not index.isValid(): return model = self.model() if model and model.graph: row = index.row() gnode = model.graph[row] return gnode.rev def context(self, rev): return self.repo.changectx(rev) def revClicked(self, index): rev = self.revFromindex(index) if rev is not None: clip = QApplication.clipboard() clip.setText(str(self.repo[rev]), QClipboard.Selection) if QApplication.keyboardModifiers() & Qt.AltModifier: self.revisionAltClicked.emit(rev) else: self.revisionClicked.emit(rev) def revActivated(self, index): rev = self.revFromindex(index) if rev is not None: self.revisionActivated.emit(rev) def onRowChange(self, index, index_from): rev = self.revFromindex(index) if self.current_rev != rev and not self._in_history: del self._rev_history[self._rev_pos+1:] self._rev_history.append(rev) self._rev_pos = len(self._rev_history)-1 self._in_history = False self.current_rev = rev self.revisionSelected.emit(rev) def selectedRevisions(self): """Return the list of selected revisions""" selmodel = self.selectionModel() return [self.revFromindex(i) for i in selmodel.selectedRows()] def gotoAncestor(self, index): rev = self.revFromindex(index) if rev is None or self.current_rev is None: return ctx = self.context(self.current_rev) ctx2 = self.context(rev) if ctx.thgmqunappliedpatch() or ctx2.thgmqunappliedpatch(): return ancestor = ctx.ancestor(ctx2) self.showMessage.emit(_("Goto ancestor of %s and %s") % ( ctx.rev(), ctx2.rev())) self.goto(ancestor.rev()) def canGoBack(self): return bool(self._rev_history and self._rev_pos > 0) def canGoForward(self): return bool(self._rev_history and self._rev_pos < len(self._rev_history) - 1) def back(self): if self.canGoBack(): self._rev_pos -= 1 idx = self.model().indexFromRev(self._rev_history[self._rev_pos]) if idx is not None: self._in_history = True self.setCurrentIndex(idx) def forward(self): if self.canGoForward(): self._rev_pos += 1 idx = self.model().indexFromRev(self._rev_history[self._rev_pos]) if idx is not None: self._in_history = True self.setCurrentIndex(idx) def goto(self, rev): """ Select revision 'rev' (can be anything understood by repo.changectx()) """ if isinstance(rev, (unicode, QString)): rev = hglib.fromunicode(rev) try: rev = self.repo.changectx(rev).rev() except error.RepoError: self.showMessage.emit(_("Can't find revision '%s'") % hglib.tounicode(str(rev))) except LookupError, e: self.showMessage.emit(hglib.tounicode(str(e))) else: idx = self.model().indexFromRev(rev) if idx is not None: # avoid unwanted selection change (#1019) if self.currentIndex().row() != idx.row(): flags = (QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) self.selectionModel().setCurrentIndex(idx, flags) self.scrollTo(idx) def saveSettings(self, s = None): if not s: s = QSettings() col_widths = [] for c in range(self.model().columnCount(QModelIndex())): col_widths.append(self.columnWidth(c)) try: key = '%s/column_widths/%s' % (self.cfgname, str(self.repo[0])) s.setValue(key, col_widths) except EnvironmentError: pass def resizeEvent(self, e): # re-size columns the smart way: the column holding Description # is re-sized according to the total widget size. if self.resized and e.oldSize().width() != e.size().width(): model = self.model() total_width = stretch_col = 0 for c in range(model.columnCount(QModelIndex())): if model._columns[c] in model._stretchs: #save the description column stretch_col = c else: #total the other widths total_width += self.columnWidth(c) width = max(self.viewport().width() - total_width, 100) self.setColumnWidth(stretch_col, width) super(HgRepoView, self).resizeEvent(e) def enablefilterpalette(self, enable): self._paletteswitcher.enablefilterpalette(enable) class HgRepoViewStyle(QStyle): "Override a style's drawPrimitive method to customize the drop indicator" def __init__(self, style): style.__class__.__init__(self) self._style = style def drawPrimitive(self, element, option, painter, widget=None): if element == QStyle.PE_IndicatorItemViewItemDrop: # Drop indicators should be painted using the full viewport width if option.rect.height() != 0: vp = widget.viewport().rect() painter.drawRect(vp.x(), option.rect.y(), vp.width() - 1, 0.5) else: self._style.drawPrimitive(element, option, painter, widget) # Delegate all other methods overridden by QProxyStyle to the base class def drawComplexControl(self, *args): return self._style.drawComplexControl(*args) def drawControl(self, *args): return self._style.drawControl(*args) def drawItemPixmap(self, *args): return self._style.drawItemPixmap(*args) def drawItemText(self, *args): return self._style.drawItemText(*args) def generatedIconPixmap(self, *args): return self._style.generatedIconPixmap(*args) def hitTestComplexControl(self, *args): return self._style.hitTestComplexControl(*args) def itemPixmapRect(self, *args): return self._style.itemPixmapRect(*args) def itemTextRect(self, *args): return self._style.itemTextRect(*args) def pixelMetric(self, *args): return self._style.pixelMetric(*args) def polish(self, *args): return self._style.polish(*args) def sizeFromContents(self, *args): return self._style.sizeFromContents(*args) def standardPalette(self): return self._style.standardPalette() def standardPixmap(self, *args): return self._style.standardPixmap(*args) def styleHint(self, *args): return self._style.styleHint(*args) def subControlRect(self, *args): return self._style.subControlRect(*args) def subElementRect(self, *args): return self._style.subElementRect(*args) def unpolish(self, *args): return self._style.unpolish(*args) def event(self, *args): return self._style.event(*args) def layoutSpacingImplementation(self, *args): return self._style.layoutSpacingImplementation(*args) def standardIconImplementation(self, *args): return self._style.standardIconImplementation(*args) class GraphDelegate(QStyledItemDelegate): def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) graph = index.data(repomodel.GraphRole) if graph: # not grayed-out even if revisions are inactive pix = QPixmap(graph) dest = option.rect src = QRect(0, 0, dest.width(), dest.height()) painter.drawPixmap(dest, pix, src) def sizeHint(self, option, index): graph = index.data(repomodel.GraphRole) if graph: return QPixmap(graph).size() else: return QSize(0, 0) tortoisehg-2.10/tortoisehg/hgqt/qscilib.py0000644000076400007640000007122412231647662020042 0ustar stevesteve# qscilib.py - Utility codes for QsciScintilla # # Copyright 2010 Steve Borho # Copyright 2010 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import re, os, weakref from mercurial import util from tortoisehg.util import hglib from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import * # indicator for highlighting preedit text of input method _IM_PREEDIT_INDIC_ID = QsciScintilla.INDIC_MAX # indicator for keyword highlighting _HIGHLIGHT_INDIC_ID = _IM_PREEDIT_INDIC_ID - 1 class _SciImSupport(object): """Patch for QsciScintilla to implement improved input method support See http://doc.trolltech.com/4.7/qinputmethodevent.html """ def __init__(self, sci): self._sci = weakref.proxy(sci) self._preeditpos = (0, 0) # (line, index) where preedit text starts self._preeditlen = 0 self._preeditcursorpos = 0 # relative pos where preedit cursor exists self._undoactionbegun = False sci.SendScintilla(QsciScintilla.SCI_INDICSETSTYLE, _IM_PREEDIT_INDIC_ID, QsciScintilla.INDIC_PLAIN) def removepreedit(self): """Remove the previous preedit text original pos: preedit cursor final pos: target cursor """ l, i = self._sci.getCursorPosition() i -= self._preeditcursorpos self._preeditcursorpos = 0 try: self._sci.setSelection( self._preeditpos[0], self._preeditpos[1], self._preeditpos[0], self._preeditpos[1] + self._preeditlen) self._sci.removeSelectedText() finally: self._sci.setCursorPosition(l, i) def commitstr(self, start, repllen, commitstr): """Remove the repl string followed by insertion of the commit string original pos: target cursor final pos: end of committed text (= start of preedit text) """ l, i = self._sci.getCursorPosition() i += start self._sci.setSelection(l, i, l, i + repllen) self._sci.removeSelectedText() self._sci.insert(commitstr) self._sci.setCursorPosition(l, i + len(commitstr)) if commitstr: self.endundo() def insertpreedit(self, text): """Insert preedit text original pos: start of preedit text final pos: start of preedit text (unchanged) """ if text and not self._preeditlen: self.beginundo() l, i = self._sci.getCursorPosition() self._sci.insert(text) self._updatepreeditpos(l, i, len(text)) if not self._preeditlen: self.endundo() def movepreeditcursor(self, pos): """Move the cursor to the relative pos inside preedit text""" self._preeditcursorpos = min(pos, self._preeditlen) l, i = self._preeditpos self._sci.setCursorPosition(l, i + self._preeditcursorpos) def beginundo(self): if self._undoactionbegun: return self._sci.beginUndoAction() self._undoactionbegun = True def endundo(self): if not self._undoactionbegun: return self._sci.endUndoAction() self._undoactionbegun = False def _updatepreeditpos(self, l, i, len): """Update the indicator and internal state for preedit text""" self._sci.SendScintilla(QsciScintilla.SCI_SETINDICATORCURRENT, _IM_PREEDIT_INDIC_ID) self._preeditpos = (l, i) self._preeditlen = len if len <= 0: # have problem on sci return p = self._sci.positionFromLineIndex(*self._preeditpos) q = self._sci.positionFromLineIndex(self._preeditpos[0], self._preeditpos[1] + len) self._sci.SendScintilla(QsciScintilla.SCI_INDICATORFILLRANGE, p, q - p) # q - p != len class ScintillaCompat(QsciScintilla): """Scintilla widget with compatibility patches""" def __init__(self, parent=None): super(ScintillaCompat, self).__init__(parent) self._imsupport = _SciImSupport(self) def inputMethodQuery(self, query): if query == Qt.ImMicroFocus: return self._cursorRect() return super(ScintillaCompat, self).inputMethodQuery(query) def inputMethodEvent(self, event): if self.isReadOnly(): return self.removeSelectedText() self._imsupport.removepreedit() self._imsupport.commitstr(event.replacementStart(), event.replacementLength(), event.commitString()) self._imsupport.insertpreedit(event.preeditString()) for a in event.attributes(): if a.type == QInputMethodEvent.Cursor: self._imsupport.movepreeditcursor(a.start) # TODO TextFormat event.accept() def _cursorRect(self): """Return a rectangle (in viewport coords) including the cursor""" l, i = self.getCursorPosition() p = self.positionFromLineIndex(l, i) x = self.SendScintilla(QsciScintilla.SCI_POINTXFROMPOSITION, 0, p) y = self.SendScintilla(QsciScintilla.SCI_POINTYFROMPOSITION, 0, p) w = self.SendScintilla(QsciScintilla.SCI_GETCARETWIDTH) return QRect(x, y, w, self.textHeight(l)) # QScintilla 2.5 can translate Backtab to Shift+SCK_TAB (issue #82) if QSCINTILLA_VERSION < 0x20500: def keyPressEvent(self, event): if event.key() == Qt.Key_Backtab: event = QKeyEvent(event.type(), Qt.Key_Tab, Qt.ShiftModifier) super(ScintillaCompat, self).keyPressEvent(event) if not hasattr(QsciScintilla, 'createStandardContextMenu'): def createStandardContextMenu(self): """Create standard context menu; ownership is transferred to caller""" menu = QMenu(self) if not self.isReadOnly(): a = menu.addAction(_('&Undo'), self.undo) a.setShortcuts(QKeySequence.Undo) a.setEnabled(self.isUndoAvailable()) a = menu.addAction(_('&Redo'), self.redo) a.setShortcuts(QKeySequence.Redo) a.setEnabled(self.isRedoAvailable()) menu.addSeparator() a = menu.addAction(_('Cu&t'), self.cut) a.setShortcuts(QKeySequence.Cut) a.setEnabled(self.hasSelectedText()) a = menu.addAction(_('&Copy'), self.copy) a.setShortcuts(QKeySequence.Copy) a.setEnabled(self.hasSelectedText()) if not self.isReadOnly(): a = menu.addAction(_('&Paste'), self.paste) a.setShortcuts(QKeySequence.Paste) a = menu.addAction(_('&Delete'), self.removeSelectedText) a.setShortcuts(QKeySequence.Delete) a.setEnabled(self.hasSelectedText()) menu.addSeparator() a = menu.addAction(_('Select &All'), self.selectAll) a.setShortcuts(QKeySequence.SelectAll) return menu # compability mode with QScintilla from Ubuntu 10.04 if not hasattr(QsciScintilla, 'HiddenIndicator'): HiddenIndicator = QsciScintilla.INDIC_HIDDEN if not hasattr(QsciScintilla, 'PlainIndicator'): PlainIndicator = QsciScintilla.INDIC_PLAIN if not hasattr(QsciScintilla, 'StrikeIndicator'): StrikeIndicator = QsciScintilla.INDIC_STRIKE if not hasattr(QsciScintilla, 'indicatorDefine'): def indicatorDefine(self, style, indicatorNumber=-1): # compatibility layer allows only one indicator to be defined if indicatorNumber == -1: indicatorNumber = 1 self.SendScintilla(self.SCI_INDICSETSTYLE, indicatorNumber, style) return indicatorNumber if not hasattr(QsciScintilla, 'setIndicatorDrawUnder'): def setIndicatorDrawUnder(self, under, indicatorNumber): self.SendScintilla(self.SCI_INDICSETUNDER, indicatorNumber, under) if not hasattr(QsciScintilla, 'setIndicatorForegroundColor'): def setIndicatorForegroundColor(self, color, indicatorNumber): self.SendScintilla(self.SCI_INDICSETFORE, indicatorNumber, color) self.SendScintilla(self.SCI_INDICSETALPHA, indicatorNumber, color.alpha()) if not hasattr(QsciScintilla, 'clearIndicatorRange'): def clearIndicatorRange(self, lineFrom, indexFrom, lineTo, indexTo, indicatorNumber): start = self.positionFromLineIndex(lineFrom, indexFrom) finish = self.positionFromLineIndex(lineTo, indexTo) self.SendScintilla(self.SCI_SETINDICATORCURRENT, indicatorNumber) self.SendScintilla(self.SCI_INDICATORCLEARRANGE, start, finish - start) if not hasattr(QsciScintilla, 'fillIndicatorRange'): def fillIndicatorRange(self, lineFrom, indexFrom, lineTo, indexTo, indicatorNumber): start = self.positionFromLineIndex(lineFrom, indexFrom) finish = self.positionFromLineIndex(lineTo, indexTo) self.SendScintilla(self.SCI_SETINDICATORCURRENT, indicatorNumber) self.SendScintilla(self.SCI_INDICATORFILLRANGE, start, finish - start) class Scintilla(ScintillaCompat): """Scintilla widget for rich file view or editor""" def __init__(self, parent=None): super(Scintilla, self).__init__(parent) self.autoUseTabs = True self.setUtf8(True) self.setWrapVisualFlags(QsciScintilla.WrapFlagByBorder) self.textChanged.connect(self._resetfindcond) self._resetfindcond() self.highlightLines = set() self._setupHighlightIndicator() self._setMultipleSelectionOptions() unbindConflictedKeys(self) def _setMultipleSelectionOptions(self): if hasattr(QsciScintilla, 'SCI_SETMULTIPLESELECTION'): self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True) self.SendScintilla(QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True) self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, QsciScintilla.SC_MULTIPASTE_EACH) self.SendScintilla(QsciScintilla.SCI_SETVIRTUALSPACEOPTIONS, QsciScintilla.SCVS_RECTANGULARSELECTION) def read(self, f): result = super(Scintilla, self).read(f) self.setDefaultEolMode() return result def contextMenuEvent(self, event): menu = self.createEditorContextMenu() menu.exec_(event.globalPos()) menu.setParent(None) def createEditorContextMenu(self): """Create context menu with editor options; ownership is transferred to caller""" menu = self.createStandardContextMenu() menu.addSeparator() editoptsmenu = menu.addMenu(_('&Editor Options')) self._buildEditorOptionsMenu(editoptsmenu) return menu def _buildEditorOptionsMenu(self, menu): qsci = QsciScintilla wrapmenu = menu.addMenu(_('&Wrap')) wrapmenu.triggered.connect(self._setWrapModeByMenu) for name, mode in ((_('&None', 'wrap mode'), qsci.WrapNone), (_('&Word'), qsci.WrapWord), (_('&Character'), qsci.WrapCharacter)): a = wrapmenu.addAction(name) a.setCheckable(True) a.setChecked(self.wrapMode() == mode) a.setData(mode) menu.addSeparator() wsmenu = menu.addMenu(_('White&space')) wsmenu.triggered.connect(self._setWhitespaceVisibilityByMenu) for name, mode in ((_('&Visible'), qsci.WsVisible), (_('&Invisible'), qsci.WsInvisible), (_('&AfterIndent'), qsci.WsVisibleAfterIndent)): a = wsmenu.addAction(name) a.setCheckable(True) a.setChecked(self.whitespaceVisibility() == mode) a.setData(mode) if not self.isReadOnly(): tabindentsmenu = menu.addMenu(_('&TAB Inserts')) tabindentsmenu.triggered.connect(self._setIndentationsUseTabsByMenu) for name, mode in ((_('&Auto'), -1), (_('&TAB'), True), (_('&Spaces'), False)): a = tabindentsmenu.addAction(name) a.setCheckable(True) a.setChecked(self.indentationsUseTabs() == mode or (self.autoUseTabs and mode == -1)) a.setData(mode) menu.addSeparator() vsmenu = menu.addMenu(_('EOL &Visibility')) vsmenu.triggered.connect(self._setEolVisibilityByMenu) for name, mode in ((_('&Visible'), True), (_('&Invisible'), False)): a = vsmenu.addAction(name) a.setCheckable(True) a.setChecked(self.eolVisibility() == mode) a.setData(mode) if not self.isReadOnly(): eolmodemenu = menu.addMenu(_('EOL &Mode')) eolmodemenu.triggered.connect(self._setEolModeByMenu) for name, mode in ((_('&Windows'), qsci.EolWindows), (_('&Unix'), qsci.EolUnix), (_('&Mac'), qsci.EolMac)): a = eolmodemenu.addAction(name) a.setCheckable(True) a.setChecked(self.eolMode() == mode) a.setData(mode) menu.addSeparator() a = menu.addAction(_('&Auto-Complete')) a.triggered.connect(self._setAutoCompletionEnabled) a.setCheckable(True) a.setChecked(self.autoCompletionThreshold() > 0) def saveSettings(self, qs, prefix): qs.setValue(prefix+'/wrap', self.wrapMode()) qs.setValue(prefix+'/whitespace', self.whitespaceVisibility()) qs.setValue(prefix+'/eol', self.eolVisibility()) if self.autoUseTabs: qs.setValue(prefix+'/usetabs', -1) else: qs.setValue(prefix+'/usetabs', self.indentationsUseTabs()) qs.setValue(prefix+'/autocomplete', self.autoCompletionThreshold()) def loadSettings(self, qs, prefix): self.setWrapMode(qs.value(prefix+'/wrap').toInt()[0]) self.setWhitespaceVisibility(qs.value(prefix+'/whitespace').toInt()[0]) self.setEolVisibility(qs.value(prefix+'/eol').toBool()) self.setIndentationsUseTabs(qs.value(prefix+'/usetabs').toInt()[0]) self.setDefaultEolMode() self.setAutoCompletionThreshold( qs.value(prefix+'/autocomplete', -1).toInt()[0]) @pyqtSlot(unicode, bool, bool, bool) def find(self, exp, icase=True, wrap=False, forward=True): """Find the next/prev occurence; returns True if found This method tries to imitate the behavior of QTextEdit.find(), unlike combo of QsciScintilla.findFirst() and findNext(). """ cond = (exp, True, not icase, False, wrap, forward) if cond == self.__findcond: return self.findNext() else: self.__findcond = cond return self.findFirst(*cond) @pyqtSlot() def _resetfindcond(self): self.__findcond = () @pyqtSlot(unicode, bool) def highlightText(self, match, icase=False): """Highlight text matching to the given regexp pattern [unicode] The previous highlight is cleared automatically. """ try: flags = 0 if icase: flags |= re.IGNORECASE pat = re.compile(unicode(match).encode('utf-8'), flags) except re.error: return # it could be partial pattern while user typing self.clearHighlightText() self.SendScintilla(self.SCI_SETINDICATORCURRENT, _HIGHLIGHT_INDIC_ID) if len(match) == 0: return # NOTE: pat and target text are *not* unicode because scintilla # requires positions in byte. For accuracy, it should do pattern # match in unicode, then calculating byte length of substring:: # # text = unicode(self.text()) # for m in pat.finditer(text): # p = len(text[:m.start()].encode('utf-8')) # self.SendScintilla(self.SCI_INDICATORFILLRANGE, # p, len(m.group(0).encode('utf-8'))) # # but it doesn't to avoid possible performance issue. for m in pat.finditer(unicode(self.text()).encode('utf-8')): self.SendScintilla(self.SCI_INDICATORFILLRANGE, m.start(), m.end() - m.start()) line = self.lineIndexFromPosition(m.start())[0] self.highlightLines.add(line) @pyqtSlot() def clearHighlightText(self): self.SendScintilla(self.SCI_SETINDICATORCURRENT, _HIGHLIGHT_INDIC_ID) self.SendScintilla(self.SCI_INDICATORCLEARRANGE, 0, self.length()) self.highlightLines.clear() def _setupHighlightIndicator(self): id = _HIGHLIGHT_INDIC_ID self.SendScintilla(self.SCI_INDICSETSTYLE, id, self.INDIC_ROUNDBOX) self.SendScintilla(self.SCI_INDICSETUNDER, id, True) self.SendScintilla(self.SCI_INDICSETFORE, id, 0x00ffff) # 0xbbggrr # alpha range is 0 to 255, but old Scintilla rejects value > 100 self.SendScintilla(self.SCI_INDICSETALPHA, id, 100) def showHScrollBar(self, show=True): self.SendScintilla(self.SCI_SETHSCROLLBAR, show) def setDefaultEolMode(self): if self.lines(): mode = qsciEolModeFromLine(unicode(self.text(0))) else: mode = qsciEolModeFromOs() self.setEolMode(mode) return mode @pyqtSlot(QAction) def _setWrapModeByMenu(self, action): mode, _ok = action.data().toInt() self.setWrapMode(mode) @pyqtSlot(QAction) def _setWhitespaceVisibilityByMenu(self, action): mode, _ok = action.data().toInt() self.setWhitespaceVisibility(mode) @pyqtSlot(QAction) def _setEolVisibilityByMenu(self, action): visible = action.data().toBool() self.setEolVisibility(visible) @pyqtSlot(QAction) def _setEolModeByMenu(self, action): mode, _ok = action.data().toInt() self.setEolMode(mode) @pyqtSlot(QAction) def _setIndentationsUseTabsByMenu(self, action): mode, _ok = action.data().toInt() self.setIndentationsUseTabs(mode) def setIndentationsUseTabs(self, tabs): self.autoUseTabs = (tabs == -1) if self.autoUseTabs and self.lines(): tabs = findTabIndentsInLines(hglib.fromunicode(self.text())) super(Scintilla, self).setIndentationsUseTabs(tabs) @pyqtSlot(bool) def _setAutoCompletionEnabled(self, enabled): self.setAutoCompletionThreshold(enabled and 2 or -1) def lineNearPoint(self, point): """Return the closest line to the pixel position; similar to lineAt(), but returns valid line number even if no character fount at point""" # lineAt() uses the strict request, SCI_POSITIONFROMPOINTCLOSE chpos = self.SendScintilla(self.SCI_POSITIONFROMPOINT, # no implicit cast to ulong in old QScintilla # unsigned long wParam, long lParam max(point.x(), 0), point.y()) return self.SendScintilla(self.SCI_LINEFROMPOSITION, chpos) class SearchToolBar(QToolBar): conditionChanged = pyqtSignal(unicode, bool, bool) """Emitted (pattern, icase, wrap) when search condition changed""" searchRequested = pyqtSignal(unicode, bool, bool, bool) """Emitted (pattern, icase, wrap, forward) when requested""" def __init__(self, parent=None, hidable=False): super(SearchToolBar, self).__init__(_('Search'), parent, objectName='search', iconSize=QSize(16, 16)) if hidable: self._close_button = QToolButton(icon=qtlib.geticon('window-close'), shortcut=Qt.Key_Escape) self._close_button.clicked.connect(self.hide) self.addWidget(self._close_button) self.addWidget(qtlib.Spacer(2, 2)) self._le = QLineEdit() if hasattr(self._le, 'setPlaceholderText'): # Qt >= 4.7 self._le.setPlaceholderText(_('### regular expression ###')) else: self._lbl = QLabel(_('Regexp:'), toolTip=_('Regular expression search pattern')) self.addWidget(self._lbl) self._lbl.setBuddy(self._le) self._le.returnPressed.connect(self._emitSearchRequested) self.addWidget(self._le) self.addWidget(qtlib.Spacer(4, 4)) self._chk = QCheckBox(_('Ignore case')) self.addWidget(self._chk) self._wrapchk = QCheckBox(_('Wrap search')) self.addWidget(self._wrapchk) self._btprev = QPushButton(_('Prev'), icon=qtlib.geticon('go-up'), iconSize=QSize(16, 16)) self._btprev.clicked.connect( lambda: self._emitSearchRequested(forward=False)) self.addWidget(self._btprev) self._bt = QPushButton(_('Next'), icon=qtlib.geticon('go-down'), iconSize=QSize(16, 16)) self._bt.clicked.connect(self._emitSearchRequested) self._le.textChanged.connect(self._updateSearchButtons) self.addWidget(self._bt) self.setFocusProxy(self._le) self.setStyleSheet(qtlib.tbstylesheet) self._settings = QSettings() self._settings.beginGroup('searchtoolbar') self.searchRequested.connect(self._writesettings) self._readsettings() self._le.textChanged.connect(self._emitConditionChanged) self._chk.toggled.connect(self._emitConditionChanged) self._wrapchk.toggled.connect(self._emitConditionChanged) self._updateSearchButtons() def keyPressEvent(self, event): if event.matches(QKeySequence.FindNext): self._emitSearchRequested(forward=True) return if event.matches(QKeySequence.FindPrevious): self._emitSearchRequested(forward=False) return if event.key() in (Qt.Key_Enter, Qt.Key_Return): return # handled by returnPressed super(SearchToolBar, self).keyPressEvent(event) def wheelEvent(self, event): if event.delta() > 0: self._emitSearchRequested(forward=False) return if event.delta() < 0: self._emitSearchRequested(forward=True) return super(SearchToolBar, self).wheelEvent(event) def setVisible(self, visible=True): super(SearchToolBar, self).setVisible(visible) if visible: self._le.setFocus() self._le.selectAll() def _readsettings(self): self.setCaseInsensitive(self._settings.value('icase', False).toBool()) self.setWrapAround(self._settings.value('wrap', False).toBool()) @pyqtSlot() def _writesettings(self): self._settings.setValue('icase', self.caseInsensitive()) self._settings.setValue('wrap', self.wrapAround()) @pyqtSlot() def _emitConditionChanged(self): self.conditionChanged.emit(self.pattern(), self.caseInsensitive(), self.wrapAround()) @pyqtSlot() def _emitSearchRequested(self, forward=True): self.searchRequested.emit(self.pattern(), self.caseInsensitive(), self.wrapAround(), forward) @pyqtSlot() def _updateSearchButtons(self): enabled = bool(self._le.text()) self._btprev.setEnabled(enabled) self._bt.setEnabled(enabled) def pattern(self): """Returns the current search pattern [unicode]""" return self._le.text() def setPattern(self, text): """Set the search pattern [unicode]""" self._le.setText(text) def caseInsensitive(self): """True if case-insensitive search is requested""" return self._chk.isChecked() def setCaseInsensitive(self, icase): self._chk.setChecked(icase) def wrapAround(self): """True if wrap search is requested""" return self._wrapchk.isChecked() def setWrapAround(self, wrap): self._wrapchk.setChecked(wrap) @pyqtSlot(unicode) def search(self, text): """Request search with the given pattern""" self.setPattern(text) self._emitSearchRequested() class KeyPressInterceptor(QObject): """Grab key press events important for dialogs Usage:: sci = qscilib.Scintilla(self) sci.installEventFilter(KeyPressInterceptor(self)) """ def __init__(self, parent=None, keys=None, keyseqs=None): super(KeyPressInterceptor, self).__init__(parent) self._keys = set((Qt.Key_Escape,)) self._keyseqs = set((QKeySequence.Refresh,)) if keys: self._keys.update(keys) if keyseqs: self._keyseqs.update(keyseqs) def eventFilter(self, watched, event): if event.type() != QEvent.KeyPress: return super(KeyPressInterceptor, self).eventFilter( watched, event) if self._isinterceptable(event): event.ignore() return True return False def _isinterceptable(self, event): if event.key() in self._keys: return True if util.any(event.matches(e) for e in self._keyseqs): return True return False def unbindConflictedKeys(sci): cmdset = sci.standardCommands() try: cmd = cmdset.boundTo(QKeySequence('CTRL+L')) if cmd: cmd.setKey(0) except AttributeError: # old QScintilla does not have boundTo() pass def qsciEolModeFromOs(): if os.name.startswith('nt'): return QsciScintilla.EolWindows else: return QsciScintilla.EolUnix def qsciEolModeFromLine(line): if line.endswith('\r\n'): return QsciScintilla.EolWindows elif line.endswith('\r'): return QsciScintilla.EolMac elif line.endswith('\n'): return QsciScintilla.EolUnix else: return qsciEolModeFromOs() def findTabIndentsInLines(lines, linestocheck=100): for line in lines[:linestocheck]: if line.startswith(' '): return False elif line.startswith('\t'): return True return False # Use spaces for indents default def fileEditor(filename, **opts): 'Open a simple modal file editing dialog' dialog = QDialog() dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) dialog.setWindowTitle(filename) dialog.setLayout(QVBoxLayout()) editor = Scintilla() editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) editor.installEventFilter(KeyPressInterceptor(dialog)) editor.setMarginLineNumbers(1, True) editor.setMarginWidth(1, '000') editor.setLexer(QsciLexerProperties()) if opts.get('foldable'): editor.setFolding(QsciScintilla.BoxedTreeFoldStyle) dialog.layout().addWidget(editor) searchbar = SearchToolBar(dialog, hidable=True) searchbar.searchRequested.connect(editor.find) searchbar.conditionChanged.connect(editor.highlightText) searchbar.hide() def showsearchbar(): text = editor.selectedText() if text: searchbar.setPattern(text) searchbar.show() searchbar.setFocus(Qt.OtherFocusReason) qtlib.newshortcutsforstdkey(QKeySequence.Find, dialog, showsearchbar) dialog.layout().addWidget(searchbar) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Save|BB.Cancel) bb.accepted.connect(dialog.accept) bb.rejected.connect(dialog.reject) dialog.layout().addWidget(bb) s = QSettings() geomname = 'editor-geom' desktopgeom = qApp.desktop().availableGeometry() dialog.resize(desktopgeom.size() * 0.5) dialog.restoreGeometry(s.value(geomname).toByteArray()) ret = QDialog.Rejected try: f = QFile(filename) f.open(QIODevice.ReadOnly) editor.read(f) editor.setModified(False) ret = dialog.exec_() if ret == QDialog.Accepted: f = QFile(filename) f.open(QIODevice.WriteOnly) editor.write(f) s.setValue(geomname, dialog.saveGeometry()) except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to read/write config file'), hglib.tounicode(str(e)), parent=dialog) return ret tortoisehg-2.10/tortoisehg/hgqt/bugreport.py0000644000076400007640000002347212135406414020416 0ustar stevesteve# bugreport.py - Report Python tracebacks to the user # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import sys import re from mercurial import encoding, extensions from tortoisehg.util import hglib, version from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest try: from PyQt4.Qsci import QSCINTILLA_VERSION_STR except (ImportError, AttributeError, RuntimeError): # show BugReport dialog even if QScintilla is missing # or incompatible (RuntimeError: the sip module implements API v...) QSCINTILLA_VERSION_STR = '(unknown)' def _safegetcwd(): try: return os.getcwd() except OSError: return '.' class BugReport(QDialog): def __init__(self, opts, parent=None): super(BugReport, self).__init__(parent) layout = QVBoxLayout() self.setLayout(layout) lbl = QLabel(_('Please report this bug to our ' 'bug tracker') % u'http://bitbucket.org/tortoisehg/thg/wiki/BugReport') lbl.setOpenExternalLinks(True) self.layout().addWidget(lbl) tb = QTextBrowser() self.text = self.gettext(opts) tb.setHtml('
' + Qt.escape(self.text) + '
') tb.setWordWrapMode(QTextOption.NoWrap) layout.addWidget(tb) self.download_url_lbl = QLabel(_('Checking for updates...')) self.download_url_lbl.setMouseTracking(True) self.download_url_lbl.setTextInteractionFlags(Qt.LinksAccessibleByMouse) self.download_url_lbl.setOpenExternalLinks(True) layout.addWidget(self.download_url_lbl) # dialog buttons BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Save) bb.button(BB.Ok).clicked.connect(self.accept) bb.button(BB.Save).clicked.connect(self.save) bb.button(BB.Ok).setDefault(True) bb.addButton(_('Copy'), BB.HelpRole).clicked.connect(self.copyText) bb.addButton(_('Quit'), BB.DestructiveRole).clicked.connect(qApp.quit) layout.addWidget(bb) self.setWindowTitle(_('TortoiseHg Bug Report')) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self.resize(650, 400) self._readsettings() QTimer.singleShot(0, self.getUpdateInfo) def getUpdateInfo(self): verurl = 'http://tortoisehg.bitbucket.org/curversion.txt' # If we use QNetworkAcessManager elsewhere, it should be shared # through the application. self._netmanager = QNetworkAccessManager(self) self._newverreply = self._netmanager.get(QNetworkRequest(QUrl(verurl))) self._newverreply.finished.connect(self.uFinished) def uFinished(self): newver = (0,0,0) try: f = self._newverreply.readAll().data().splitlines() self._newverreply.close() self._newverreply = None newver = tuple([int(p) for p in f[0].split('.')]) upgradeurl = f[1] # generic download URL platform = sys.platform if platform == 'win32': from win32process import IsWow64Process as IsX64 platform = IsX64() and 'x64' or 'x86' # linux2 for Linux, darwin for OSX for line in f[2:]: p, _url = line.split(':', 1) if platform == p: upgradeurl = _url.strip() break except (IndexError, ImportError, ValueError): pass try: thgv = version.version() if '+' in thgv: thgv = thgv[:thgv.index('+')] curver = tuple([int(p) for p in thgv.split('.')]) except ValueError: curver = (0,0,0) if newver > curver: url_lbl = _('Upgrading to a more recent TortoiseHg is recommended.') urldata = ('%s' % (upgradeurl, url_lbl)) self.download_url_lbl.setText(urldata) else: self.download_url_lbl.setText(_('Your TortoiseHg is up to date.')) def gettext(self, opts): # TODO: make this more uniformly unicode safe text = '#!python\n' # Bitbucket wiki marker for python code text += '** Mercurial version (%s). TortoiseHg version (%s)\n' % ( hglib.hgversion, version.version()) text += '** Command: %s\n' % (hglib.tounicode(opts.get('cmd', 'N/A'))) text += '** CWD: %s\n' % hglib.tounicode(_safegetcwd()) text += '** Encoding: %s\n' % encoding.encoding extlist = [x[0] for x in extensions.extensions()] text += '** Extensions loaded: %s\n' % ', '.join(extlist) text += '** Python version: %s\n' % sys.version.replace('\n', '') if os.name == 'nt': text += self.getarch() elif os.name == 'posix': text += '** System: %s\n' % hglib.tounicode(' '.join(os.uname())) text += ('** Qt-%s PyQt-%s QScintilla-%s\n' % (QT_VERSION_STR, PYQT_VERSION_STR, QSCINTILLA_VERSION_STR)) text += hglib.tounicode(opts.get('error', 'N/A')) # Bitbucket wiki marker for code: 4 spaces indent (Markdown syntax) regexp = re.compile(r'^', re.MULTILINE) text = regexp.sub(r' ', text) return text def copyText(self): QApplication.clipboard().setText(self.text) def getarch(self): text = '** Windows version: %s\n' % str(sys.getwindowsversion()) arch = 'unknown (failed to import win32api)' try: import win32api arch = 'unknown' archval = win32api.GetNativeSystemInfo()[0] if archval == 9: arch = 'x64' elif archval == 0: arch = 'x86' except (ImportError, AttributeError): pass text += '** Processor architecture: %s\n' % arch return text def save(self): try: fname = QFileDialog.getSaveFileName(self, _('Save error report to'), os.path.join(_safegetcwd(), 'bugreport.txt'), _('Text files (*.txt)')) if fname: open(fname, 'wb').write(hglib.fromunicode(self.text)) except (EnvironmentError), e: QMessageBox.critical(self, _('Error writing file'), str(e)) def accept(self): self._writesettings() super(BugReport, self).accept() def reject(self): self._writesettings() super(BugReport, self).reject() def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('bugreport/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('bugreport/geom', self.saveGeometry()) class ExceptionMsgBox(QDialog): """Message box for recoverable exception""" def __init__(self, main, text, opts, parent=None): super(ExceptionMsgBox, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowTitle(_('TortoiseHg Error')) self._opts = opts labelflags = Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse self.setLayout(QVBoxLayout()) if '%(arg' in text: values = opts.get('values', []) msgopts = {} for i, val in enumerate(values): msgopts['arg' + str(i)] = Qt.escape(hglib.tounicode(val)) try: text = text % msgopts except Exception, e: print e, msgopts else: self._mainlabel = QLabel('%s' % Qt.escape(main), textInteractionFlags=labelflags) self.layout().addWidget(self._mainlabel) text = text + "

" + _('If you still have trouble, ' 'please file a bug report.') self._textlabel = QLabel(text, wordWrap=True, textInteractionFlags=labelflags) self._textlabel.linkActivated.connect(self._openlink) self._textlabel.setWordWrap(False) self.layout().addWidget(self._textlabel) bb = QDialogButtonBox(QDialogButtonBox.Close, centerButtons=True) bb.rejected.connect(self.reject) self.layout().addWidget(bb) desktopgeom = qApp.desktop().availableGeometry() self.resize(desktopgeom.size() * 0.20) @pyqtSlot(QString) def _openlink(self, ref): ref = str(ref) if ref == '#bugreport': return BugReport(self._opts, self).exec_() if ref.startswith('#edit:'): fname, lineno = ref[6:].rsplit(':', 1) try: # A chicken-egg problem here, we need a ui to get your # editor in order to repair your ui config file. from mercurial import ui as uimod from tortoisehg.hgqt import qtlib class FakeRepo(object): def __init__(self): self.root = os.getcwd() self.ui = uimod.ui() fake = FakeRepo() qtlib.editfiles(fake, [fname], lineno, parent=self) except Exception, e: qtlib.openlocalurl(fname) if ref.startswith('#fix:'): from tortoisehg.hgqt import settings errtext = ref[5:].split(' ')[0] sd = settings.SettingsDialog(configrepo=False, focus=errtext, parent=self, root='') sd.exec_() def run(ui, *pats, **opts): return BugReport(opts) if __name__ == "__main__": app = QApplication(sys.argv) form = BugReport({'cmd':'cmd', 'error':'error'}) form.show() app.exec_() tortoisehg-2.10/tortoisehg/hgqt/purge.py0000644000076400007640000002257112231647662017537 0ustar stevesteve# purge.py - working copy purge dialog, based on Mercurial purge extension # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import stat import shutil from mercurial import hg, scmutil, ui from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _, ngettext from tortoisehg.hgqt import qtlib, cmdui from PyQt4.QtCore import * from PyQt4.QtGui import * class PurgeDialog(QDialog): progress = pyqtSignal(QString, object, QString, QString, object) showMessage = pyqtSignal(QString) def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent layout = QVBoxLayout() layout.setMargin(0) layout.setSpacing(0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setMargin(10) toplayout.setSpacing(5) layout.addLayout(toplayout) cb = QCheckBox(_('No unknown files found')) cb.setChecked(False) cb.setEnabled(False) toplayout.addWidget(cb) self.ucb = cb cb = QCheckBox(_('No ignored files found')) cb.setChecked(False) cb.setEnabled(False) toplayout.addWidget(cb) self.icb = cb cb = QCheckBox(_('No trash files found')) cb.setChecked(False) cb.setEnabled(False) toplayout.addWidget(cb) self.tcb = cb self.foldercb = QCheckBox(_('Delete empty folders')) self.foldercb.setChecked(True) toplayout.addWidget(self.foldercb) self.hgfilecb = QCheckBox(_('Preserve files beginning with .hg')) self.hgfilecb.setChecked(True) toplayout.addWidget(self.hgfilecb) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.bb = bb toplayout.addStretch() toplayout.addWidget(bb) self.stbar = cmdui.ThgStatusBar(self) self.progress.connect(self.stbar.progress) self.showMessage.connect(self.stbar.showMessage) layout.addWidget(self.stbar) repo = repoagent.rawRepo() self.setWindowTitle(_('%s - purge') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-purge')) self.bb.setEnabled(False) self.progress.emit(*cmdui.startProgress(_('Checking'), '...')) s = QSettings() desktopgeom = qApp.desktop().availableGeometry() self.resize(desktopgeom.size() * 0.25) self.restoreGeometry(s.value('purge/geom').toByteArray()) self.th = None QTimer.singleShot(0, self.checkStatus) @property def repo(self): return self._repoagent.rawRepo() def checkStatus(self): repo = self.repo class CheckThread(QThread): def __init__(self, parent): QThread.__init__(self, parent) self.files = (None, None) self.error = None def run(self): try: repo.bfstatus = True repo.lfstatus = True stat = repo.status(ignored=True, unknown=True) repo.bfstatus = False repo.lfstatus = False trashcan = repo.join('Trashcan') if os.path.isdir(trashcan): trash = os.listdir(trashcan) else: trash = [] self.files = stat[4], stat[5], trash except Exception, e: self.error = str(e) self.th = CheckThread(self) self.th.finished.connect(self._checkCompleted) self.th.start() @pyqtSlot() def _checkCompleted(self): self.th.wait() self.files = self.th.files self.bb.setEnabled(True) self.progress.emit(*cmdui.stopProgress(_('Checking'))) if self.th.error: self.showMessage.emit(hglib.tounicode(self.th.error)) else: self.showMessage.emit(_('Ready to purge.')) U, I, T = self.files if U: self.ucb.setText(ngettext( 'Delete %d unknown file', 'Delete %d unknown files', len(U)) % len(U)) self.ucb.setChecked(True) self.ucb.setEnabled(True) if I: self.icb.setText(ngettext( 'Delete %d ignored file', 'Delete %d ignored files', len(I)) % len(I)) self.icb.setChecked(True) self.icb.setEnabled(True) if T: self.tcb.setText(ngettext( 'Delete %d file in .hg/Trashcan', 'Delete %d files in .hg/Trashcan', len(T)) % len(T)) self.tcb.setChecked(True) self.tcb.setEnabled(True) def reject(self): if self.th and self.th.isRunning(): return s = QSettings() s.setValue('purge/geom', self.saveGeometry()) super(PurgeDialog, self).reject() def accept(self): unknown = self.ucb.isChecked() ignored = self.icb.isChecked() trash = self.tcb.isChecked() delfolders = self.foldercb.isChecked() keephg = self.hgfilecb.isChecked() if not (unknown or ignored or trash or delfolders): QDialog.accept(self) return if not qtlib.QuestionMsgBox(_('Confirm file deletions'), _('Are you sure you want to delete these files and/or folders?'), parent=self): return opts = dict(unknown=unknown, ignored=ignored, trash=trash, delfolders=delfolders, keephg=keephg) self.th = PurgeThread(self.repo, opts, self) self.th.progress.connect(self.progress) self.th.showMessage.connect(self.showMessage) self.th.finished.connect(self._purgeCompleted) self.th.start() @pyqtSlot() def _purgeCompleted(self): self.th.wait() F = self.th.failures if F: qtlib.InfoMsgBox(_('Deletion failures'), ngettext( 'Unable to delete %d file or folder', 'Unable to delete %d files or folders', len(F)) % len(F), parent=self) if F is not None: self.reject() class PurgeThread(QThread): progress = pyqtSignal(QString, object, QString, QString, object) showMessage = pyqtSignal(QString) def __init__(self, repo, opts, parent): super(PurgeThread, self).__init__(parent) self.failures = 0 self.root = repo.root self.opts = opts def run(self): try: self.failures = self.purge(self.root, self.opts) except Exception, e: self.failures = None self.showMessage.emit(hglib.tounicode(str(e))) def purge(self, root, opts): repo = hg.repository(ui.ui(), self.root) keephg = opts['keephg'] directories = [] failures = [] if opts['trash']: self.showMessage.emit(_('Deleting trash folder...')) trashcan = repo.join('Trashcan') try: shutil.rmtree(trashcan) except EnvironmentError: failures.append(trashcan) self.showMessage.emit('') match = scmutil.matchall(repo) match.explicitdir = match.traversedir = directories.append repo.bfstatus = True repo.lfstatus = True status = repo.status(match=match, ignored=opts['ignored'], unknown=opts['unknown'], clean=False) repo.bfstatus = False repo.lfstatus = False files = status[4] + status[5] def remove(remove_func, name): try: if keephg and name.startswith('.hg'): return remove_func(repo.wjoin(name)) except EnvironmentError: failures.append(name) def removefile(path): try: os.remove(path) except OSError: # read-only files cannot be unlinked under Windows s = os.stat(path) if (s.st_mode & stat.S_IWRITE) != 0: raise os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE) os.remove(path) for i, f in enumerate(sorted(files)): data = ('deleting', i, f, '', len(files)) self.progress.emit(*data) remove(removefile, f) data = ('deleting', None, '', '', len(files)) self.progress.emit(*data) self.showMessage.emit(_('Deleted %d files') % len(files)) if opts['delfolders'] and directories: for i, f in enumerate(sorted(directories, reverse=True)): if match(f) and not os.listdir(repo.wjoin(f)): data = ('rmdir', i, f, '', len(directories)) self.progress.emit(*data) remove(os.rmdir, f) data = ('rmdir', None, f, '', len(directories)) self.progress.emit(*data) self.showMessage.emit(_('Deleted %d files and %d folders') % ( len(files), len(directories))) return failures tortoisehg-2.10/tortoisehg/hgqt/filedata.py0000644000076400007640000005247712235634453020174 0ustar stevesteve# filedata.py - generate displayable file data # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import cStringIO from mercurial import error, match, patch, util, mdiff from mercurial import ui as uimod from hgext import record from tortoisehg.util import hglib, patchctx from tortoisehg.hgqt.i18n import _ forcedisplaymsg = _('Display the file anyway') def _exceedsMaxLineLength(data, maxlength=100000): if len(data) < maxlength: return False for line in data.splitlines(): if len(line) > maxlength: return True return False class FileData(object): def __init__(self, ctx, ctx2, wfile, status=None, changeselect=False, force=False): self.contents = None self.ucontents = None self.error = None self.olddata = None self.diff = None self.flabel = u'' self.elabel = u'' self.changes = None try: self.readStatus(ctx, ctx2, wfile, status, changeselect, force) except (EnvironmentError, error.LookupError), e: self.error = hglib.tounicode(str(e)) def checkMaxDiff(self, ctx, wfile, maxdiff, status, force): self.error = None p = _('File or diffs not displayed: ') try: fctx = ctx.filectx(wfile) if ctx.rev() is None: size = fctx.size() else: # fctx.size() can read all data into memory in rename cases so # we read the size directly from the filelog, this is deeper # under the API than I prefer to go, but seems necessary size = fctx._filelog.rawsize(fctx.filerev()) except (EnvironmentError, error.LookupError), e: self.error = p + hglib.tounicode(str(e)) return None if not force and size > maxdiff: self.error = p + _('File is larger than the specified max size.\n' 'maxdiff = %s KB') % (maxdiff // 1024) self.error += u'\n\n' + forcedisplaymsg return None try: data = fctx.data() if not force: if '\0' in data or ctx.isStandin(wfile): self.error = p + _('File is binary') elif _exceedsMaxLineLength(data): # it's incredibly slow to render long line by QScintilla self.error = p + \ _('File may be binary (maximum line length exceeded)') if self.error: self.error += u'\n\n' + forcedisplaymsg if status != 'A': return None renamed = fctx.renamed() if renamed: oldname, node = renamed fr = hglib.tounicode(oldname) if oldname in ctx: self.flabel += _(' (copied from %s)') % fr else: self.flabel += _(' (renamed from %s)') % fr else: self.flabel += _(' (was added)') return None except (EnvironmentError, util.Abort), e: self.error = p + hglib.tounicode(str(e)) return None return fctx, data def isValid(self): return self.error is None def readStatus(self, ctx, ctx2, wfile, status, changeselect, force): def getstatus(repo, n1, n2, wfile): m = match.exact(repo.root, repo.getcwd(), [wfile]) modified, added, removed = repo.status(n1, n2, match=m)[:3] if wfile in modified: return 'M' if wfile in added: return 'A' if wfile in removed: return 'R' if wfile in ctx: return 'C' return None isbfile = False repo = ctx._repo maxdiff = repo.maxdiff self.flabel += u'%s' % hglib.tounicode(wfile) if isinstance(ctx, patchctx.patchctx): self.diff = ctx.thgmqpatchdata(wfile) flags = ctx.flags(wfile) if flags == 'x': self.elabel = _("exec mode has been " "set") elif flags == '-': self.elabel = _("exec mode has been " "unset") elif flags == 'l': self.flabel += _(' (is a symlink)') # Do not show patches that are too big or may be binary if not force: p = _('Diff not displayed: ') data = self.diff size = len(data) if (size > maxdiff): self.error = p + _('File is larger than the specified max ' 'size.\n' 'maxdiff = %s KB') % (maxdiff // 1024) elif '\0' in data: self.error = p + _('File is binary') elif _exceedsMaxLineLength(data): # it's incredibly slow to render long line by QScintilla self.error = p + \ _('File may be binary (maximum line length exceeded)') if self.error: self.error += u'\n\n' + forcedisplaymsg return if ctx2: # If a revision to compare to was provided, we must put it in # the context of the subrepo as well if ctx2._repo.root != ctx._repo.root: wsub2, wfileinsub2, sctx2 = \ hglib.getDeepestSubrepoContainingFile(wfile, ctx2) if wsub2: ctx2 = sctx2 absfile = repo.wjoin(wfile) if (wfile in ctx and 'l' in ctx.flags(wfile)) or \ os.path.islink(absfile): if wfile in ctx: data = ctx[wfile].data() else: data = os.readlink(absfile) self.contents = data self.flabel += _(' (is a symlink)') return if status is None: status = getstatus(repo, ctx.p1().node(), ctx.node(), wfile) if ctx2 is None: ctx2 = ctx.p1() if status == 'S': try: from mercurial import subrepo, commands def genSubrepoRevChangedDescription(subrelpath, sfrom, sto, repo): """Generate a subrepository revision change description""" out = [] def getLog(_ui, srepo, opts): if srepo is None: return _('changeset: %s') % opts['rev'][0][:12] _ui.pushbuffer() logOutput = '' try: commands.log(_ui, srepo, **opts) logOutput = _ui.popbuffer() if not logOutput: return _('Initial revision') + u'\n' except error.ParseError, e: # Some mercurial versions have a bug that results in # saving a subrepo node id in the .hgsubstate file # which ends with a "+" character. If that is the # case, add a warning to the output, but try to # get the revision information anyway for n, rev in enumerate(opts['rev']): if rev.endswith('+'): logOutput += _('[WARNING] Invalid subrepo ' 'revision ID:\n\t%s\n\n') % rev opts['rev'][n] = rev[:-1] commands.log(_ui, srepo, **opts) logOutput += _ui.popbuffer() return hglib.tounicode(logOutput) opts = {'date':None, 'user':None, 'rev':[sfrom]} subabspath = os.path.join(repo.root, subrelpath) missingsub = srepo is None or not os.path.isdir(subabspath) sfromlog = '' def isinitialrevision(rev): return all([el == '0' for el in rev]) if isinitialrevision(sfrom): sfrom = '' if isinitialrevision(sto): sto = '' header = '' if not sfrom and not sto: sstatedesc = 'new' out.append(_('Subrepo created and set to initial ' 'revision.') + u'\n\n') return out, sstatedesc elif not sfrom: sstatedesc = 'new' header = _('Subrepo initialized to revision:') + u'\n\n' elif not sto: sstatedesc = 'removed' out.append(_('Subrepo removed from repository.') + u'\n\n') out.append(_('Previously the subrepository was ' 'at the following revision:') + u'\n\n') subinfo = getLog(_ui, srepo, {'rev': [sfrom]}) slog = hglib.tounicode(subinfo) out.append(slog) return out, sstatedesc elif sfrom == sto: sstatedesc = 'unchanged' header = _('Subrepo was not changed.') slog = _('changeset: %s') % sfrom[:12] + u'\n' if missingsub: header = _('[WARNING] Missing subrepo. ' 'Update to this revision to clone it.') \ + u'\n\n' + header else: try: slog = getLog(_ui, srepo, opts) except error.RepoError: header = _('[WARNING] Incomplete subrepo. ' 'Update to this revision to pull it.') \ + u'\n\n' + header out.append(header + u' ') out.append(_('Subrepo state is:') + u'\n\n' + slog) return out, sstatedesc else: sstatedesc = 'changed' header = _('Revision has changed to:') + u'\n\n' sfromlog = _('changeset: %s') % sfrom[:12] + u'\n\n' if not missingsub: try: sfromlog = getLog(_ui, srepo, opts) except error.RepoError: sfromlog = _('changeset: %s ' '(not found on subrepository)') \ % sfrom[:12] + u'\n\n' sfromlog = _('From:') + u'\n' + sfromlog stolog = '' if missingsub: header = _( '[WARNING] Missing changed subrepository. ' 'Update to this revision to clone it.') \ + u'\n\n' + header stolog = _('changeset: %s') % sto[:12] + '\n\n' sfromlog += _( 'Subrepository not found in the working ' 'directory.') + '\n' else: try: opts['rev'] = [sto] stolog = getLog(_ui, srepo, opts) except error.RepoError: header = _( '[WARNING] Incomplete changed subrepository. ' 'Update to this revision to pull it.') \ + u'\n\n' + header stolog = _('changeset: %s ' '(not found on subrepository)') \ % sto[:12] + u'\n\n' out.append(header) out.append(stolog) if sfromlog: out.append(sfromlog) return out, sstatedesc srev = ctx.substate.get(wfile, subrepo.nullstate)[1] srepo = None subabspath = os.path.join(ctx._repo.root, wfile) sactual = '' if os.path.isdir(subabspath): try: sub = ctx.sub(wfile) if isinstance(sub, subrepo.hgsubrepo): srepo = sub._repo if srepo is not None: sactual = srepo['.'].hex() else: self.error = _('Not a Mercurial subrepo, not ' 'previewable') return except util.Abort, e: self.error = (_('Error previewing subrepo: %s') % hglib.tounicode(str(e))) + u'\n\n' self.error += _('Subrepo may be damaged or ' 'inaccessible.') return except KeyError, e: # Missing, incomplete or removed subrepo. # Will be handled later as such below pass out = [] _ui = uimod.ui() if srepo is None or ctx.rev() is not None: data = [] else: _ui.pushbuffer() commands.status(_ui, srepo, modified=True, added=True, removed=True, deleted=True) data = _ui.popbuffer() if data: out.append(_('The subrepository is dirty.') + u' ' + _('File Status:') + u'\n') out.append(hglib.tounicode(data)) out.append(u'\n') sstatedesc = 'changed' if ctx.rev() is not None: sparent = ctx2.substate.get(wfile, subrepo.nullstate)[1] subrepochange, sstatedesc = \ genSubrepoRevChangedDescription(wfile, sparent, srev, ctx._repo) out += subrepochange else: sstatedesc = 'dirty' if srev != sactual: subrepochange, sstatedesc = \ genSubrepoRevChangedDescription(wfile, srev, sactual, ctx._repo) out += subrepochange if data: sstatedesc += ' and dirty' elif srev and not sactual: sstatedesc = 'removed' self.ucontents = u''.join(out).strip() lbl = { 'changed': _('(is a changed sub-repository)'), 'unchanged': _('(is an unchanged sub-repository)'), 'dirty': _('(is a dirty sub-repository)'), 'new': _('(is a new sub-repository)'), 'removed': _('(is a removed sub-repository)'), 'changed and dirty': _('(is a changed and dirty ' 'sub-repository)'), 'new and dirty': _('(is a new and dirty sub-repository)'), 'removed and dirty': _('(is a removed sub-repository)') }[sstatedesc] self.flabel += ' ' + lbl + '' if sactual: lbl = ' %s' % _('open...') self.flabel += lbl % hglib.tounicode(srepo.root) except (EnvironmentError, error.RepoError, util.Abort), e: self.error = _('Error previewing subrepo: %s') % \ hglib.tounicode(str(e)) return # TODO: elif check if a subdirectory (for manifest tool) mde = _('File or diffs not displayed: ' 'File is larger than the specified max size.\n' 'maxdiff = %s KB') % (maxdiff // 1024) if status in ('R', '!'): if wfile in ctx.p1(): fctx = ctx.p1()[wfile] if fctx._filelog.rawsize(fctx.filerev()) > maxdiff: self.error = mde else: olddata = fctx.data() if '\0' in olddata: self.error = 'binary file' else: self.contents = olddata self.flabel += _(' (was deleted)') elif hasattr(ctx.p1(), 'hasStandin') and ctx.p1().hasStandin(wfile): self.error = 'binary file' self.flabel += _(' (was deleted)') else: self.flabel += _(' (was added, now missing)') return if status in ('I', '?'): assert ctx.rev() is None self.flabel += _(' (is unversioned)') if os.path.getsize(absfile) > maxdiff: self.error = mde return data = util.posixfile(absfile, 'r').read() if not force and '\0' in data: self.error = 'binary file' else: self.contents = data return if status in ('M', 'A', 'C'): if ctx.hasStandin(wfile): wfile = ctx.findStandin(wfile) isbfile = True res = self.checkMaxDiff(ctx, wfile, maxdiff, status, force) if res is None: return fctx, newdata = res self.contents = newdata if status == 'C': # no further comparison is necessary return for pctx in ctx.parents(): if 'x' in fctx.flags() and 'x' not in pctx.flags(wfile): self.elabel = _("exec mode has been " "set") elif 'x' not in fctx.flags() and 'x' in pctx.flags(wfile): self.elabel = _("exec mode has been " "unset") if status == 'A': renamed = fctx.renamed() if not renamed: self.flabel += _(' (was added)') return oldname, node = renamed fr = hglib.tounicode(oldname) if oldname in ctx: self.flabel += _(' (copied from %s)') % fr else: self.flabel += _(' (renamed from %s)') % fr olddata = repo.filectx(oldname, fileid=node).data() elif status == 'M': if wfile not in ctx2: # merge situation where file was added in other branch self.flabel += _(' (was added)') return oldname = wfile olddata = ctx2[wfile].data() else: return self.olddata = olddata if changeselect: diffopts = patch.diffopts(repo.ui, {}) diffopts.git = True m = match.exact(repo.root, repo.root, [wfile]) fp = cStringIO.StringIO() for c in patch.diff(repo, ctx.node(), None, match=m, opts=diffopts): fp.write(c) fp.seek(0) # feed diffs through record.parsepatch() for more fine grained # chunk selection filediffs = record.parsepatch(fp) if filediffs: self.changes = filediffs[0] else: self.diff = '' return self.changes.excludecount = 0 values = [] lines = 0 for chunk in self.changes.hunks: buf = cStringIO.StringIO() chunk.write(buf) chunk.excluded = False val = buf.getvalue() values.append(val) chunk.lineno = lines chunk.linecount = len(val.splitlines()) lines += chunk.linecount self.diff = ''.join(values) else: diffopts = patch.diffopts(repo.ui, {}) diffopts.git = False newdate = util.datestr(ctx.date()) olddate = util.datestr(ctx2.date()) if isbfile: olddata += '\0' newdata += '\0' difftext = mdiff.unidiff(olddata, olddate, newdata, newdate, oldname, wfile, opts=diffopts) if difftext: self.diff = ('diff -r %s -r %s %s\n' % (ctx, ctx2, oldname) + difftext) else: self.diff = '' tortoisehg-2.10/tortoisehg/hgqt/webconf.ui0000664000076400007640000000614012100577421020007 0ustar stevesteve WebconfForm 0 0 455 300 Webconf Config File: path_edit 0 0 QComboBox::InsertAtTop Open Save Qt::Horizontal 0 false false Add Edit Remove Qt::Vertical 0 0 tortoisehg-2.10/tortoisehg/hgqt/sign.py0000644000076400007640000001516112231647662017352 0ustar stevesteve# sign.py - Sign dialog for TortoiseHg # # Copyright 2013 Elson Wei # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, qtlib class SignDialog(QDialog): def __init__(self, repoagent, rev='tip', parent=None, opts={}): super(SignDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self.rev = rev # base layout box base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(*(0,)*4) base.setSizeConstraint(QLayout.SetFixedSize) self.setLayout(base) box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(8,)*4) self.layout().addLayout(box) ## main layout grid form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) box.addLayout(form) form.addRow(_('Revision:'), QLabel('%s (%s)' % (rev, repo[rev]))) ### key line edit self.keyLineEdit = QLineEdit() form.addRow(_('Key:'), self.keyLineEdit) ### options expander = qtlib.ExpanderLabel(_('Options'), False) expander.expanded.connect(self.show_options) box.addWidget(expander) optbox = QVBoxLayout() optbox.setSpacing(6) box.addLayout(optbox) hbox = QHBoxLayout() hbox.setSpacing(0) optbox.addLayout(hbox) self.localCheckBox = QCheckBox(_('Local sign')) self.localCheckBox.toggled.connect(self.updateStates) optbox.addWidget(self.localCheckBox) self.replaceCheckBox = QCheckBox(_('Sign even if the sigfile is ' 'modified (-f/--force)')) self.replaceCheckBox.toggled.connect(self.updateStates) optbox.addWidget(self.replaceCheckBox) self.nocommitCheckBox = QCheckBox(_('No commit')) self.nocommitCheckBox.toggled.connect(self.updateStates) optbox.addWidget(self.nocommitCheckBox) self.customCheckBox = QCheckBox(_('Use custom commit message:')) self.customCheckBox.toggled.connect(self.customMessageToggle) optbox.addWidget(self.customCheckBox) self.customTextLineEdit = QLineEdit() optbox.addWidget(self.customTextLineEdit) ## bottom buttons BB = QDialogButtonBox bbox = QDialogButtonBox() self.signBtn = bbox.addButton(_('&Sign'), BB.ActionRole) bbox.addButton(BB.Close) bbox.rejected.connect(self.reject) box.addWidget(bbox) self.signBtn.clicked.connect(self.onSign) ## horizontal separator self.sep = QFrame() self.sep.setFrameShadow(QFrame.Sunken) self.sep.setFrameShape(QFrame.HLine) self.layout().addWidget(self.sep) ## status line self.status = qtlib.StatusLabel() self.status.setContentsMargins(4, 2, 4, 4) self.layout().addWidget(self.status) # prepare to show self.setWindowTitle(_('Sign - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-sign')) self.clear_status() key = opts.get('key', '') if not key: key = repo.ui.config("gpg", "key", '') self.keyLineEdit.setText(hglib.tounicode(key)) self.replaceCheckBox.setChecked(bool(opts.get('force'))) self.localCheckBox.setChecked(bool(opts.get('local'))) self.nocommitCheckBox.setChecked(bool(opts.get('no_commit'))) msg = opts.get('message', '') self.customTextLineEdit.setText(hglib.tounicode(msg)) if msg: self.customCheckBox.setChecked(True) self.customMessageToggle(True) else: self.customCheckBox.setChecked(False) self.customMessageToggle(False) self.keyLineEdit.setFocus() expanded = util.any([self.replaceCheckBox.isChecked(), self.localCheckBox.isChecked(), self.nocommitCheckBox.isChecked(), self.customCheckBox.isChecked() ]) expander.set_expanded(expanded) self.show_options(expanded) self.updateStates() @property def repo(self): return self._repoagent.rawRepo() def show_options(self, visible): self.localCheckBox.setVisible(visible) self.replaceCheckBox.setVisible(visible) self.nocommitCheckBox.setVisible(visible) self.customCheckBox.setVisible(visible) self.customTextLineEdit.setVisible(visible) def commandFinished(self, ret): if ret == 0: self.set_status(_("Signature has been added")) else: self.set_status(self._cmdsession.errorString(), False) @pyqtSlot() def updateStates(self): nocommit = self.nocommitCheckBox.isChecked() custom = self.customCheckBox.isChecked() self.customCheckBox.setEnabled(not nocommit) self.customTextLineEdit.setEnabled(not nocommit and custom) def onSign(self): if not self._cmdsession.isFinished(): self.set_status(_('Repository command still running'), False) return opts = { 'key': self.keyLineEdit.text() or None, 'local': self.localCheckBox.isChecked(), 'force': self.replaceCheckBox.isChecked(), 'no_commit': self.nocommitCheckBox.isChecked(), } if self.customCheckBox.isChecked() and not opts['no_commit']: opts['message'] = self.customTextLineEdit.text() or None user = qtlib.getCurrentUsername(self, self.repo) if not user: return opts['user'] = hglib.tounicode(user) cmdline = hglib.buildcmdargs('sign', self.rev, **opts) sess = self._repoagent.runCommand(cmdline, self) self._cmdsession = sess sess.commandFinished.connect(self.commandFinished) def customMessageToggle(self, checked): self.customTextLineEdit.setEnabled(checked) if checked: self.customTextLineEdit.setFocus() def set_status(self, text, icon=None): self.status.setShown(True) self.sep.setShown(True) self.status.set_status(text, icon) def clear_status(self): self.status.setHidden(True) self.sep.setHidden(True) tortoisehg-2.10/tortoisehg/hgqt/qrename.py0000644000076400007640000000706412231647662020045 0ustar stevesteve# qrename.py - QRename dialog for TortoiseHg # # Copyright 2010 Steve Borho # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib def checkPatchname(patchfile, parent): if os.path.exists(patchfile): dlg = CheckPatchnameDialog(os.path.basename(patchfile), parent) choice = dlg.exec_() if choice == 1: # add .OLD to existing patchfile try: os.rename(patchfile, patchfile + '.OLD') except (OSError, IOError), inst: qtlib.ErrorMsgBox(_('Rename Error'), _('Could not rename existing patchfile'), hglib.tounicode(str(inst))) return False return True elif choice == 2: # overwite existing patchfile try: os.remove(patchfile) except (OSError, IOError), inst: qtlib.ErrorMsgBox(_('Rename Error'), _('Could not delete existing patchfile'), hglib.tounicode(str(inst))) return False return True elif choice == 3: # go back and change the new name return False else: return False else: return True class CheckPatchnameDialog(QDialog): def __init__(self, patchname, parent): super(CheckPatchnameDialog, self).__init__(parent) self.setWindowTitle(_('QRename - Check patchname')) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self.patchname = patchname self.vbox = QVBoxLayout() self.vbox.setSpacing(4) lbl = QLabel(_('Patch name %s already exists:') % (self.patchname)) self.vbox.addWidget(lbl) self.extensionradio = \ QRadioButton(_('Add .OLD extension to existing patchfile')) self.vbox.addWidget(self.extensionradio) self.overwriteradio = QRadioButton(_('Overwrite existing patchfile')) self.vbox.addWidget(self.overwriteradio) self.backradio = QRadioButton(_('Go back and change new patchname')) self.vbox.addWidget(self.backradio) self.extensionradio.toggled.connect(self.onExtensionRadioChecked) self.overwriteradio.toggled.connect(self.onOverwriteRadioChecked) self.backradio.toggled.connect(self.onBackRadioChecked) self.choice = 0 self.extensionradio.setChecked(True) self.extensionradio.setFocus() self.setLayout(self.vbox) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Ok|BB.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) self.bbox = bbox @pyqtSlot() def onExtensionRadioChecked(self): if self.extensionradio.isChecked(): self.choice = 1 @pyqtSlot() def onOverwriteRadioChecked(self): if self.overwriteradio.isChecked(): self.choice = 2 @pyqtSlot() def onBackRadioChecked(self): if self.backradio.isChecked(): self.choice = 3 def accept(self): self.done(self.choice) self.close() def reject(self): self.done(0) tortoisehg-2.10/tortoisehg/hgqt/qtlib.py0000644000076400007640000014106412231647662017527 0ustar stevesteve# qtlib.py - Qt utility code # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import sys import atexit import shutil import shlex import stat import subprocess import tempfile import re import weakref from mercurial.i18n import _ as hggettext from mercurial import commands, extensions, error, util from tortoisehg.util import hglib, paths, editor, terminal from tortoisehg.hgqt.i18n import _ from hgext.color import _styles from PyQt4.QtCore import * from PyQt4.QtGui import * try: import win32con openflags = win32con.CREATE_NO_WINDOW except ImportError: openflags = 0 tmproot = None def gettempdir(): global tmproot def cleanup(): def writeable(arg, dirname, names): for name in names: fullname = os.path.join(dirname, name) os.chmod(fullname, os.stat(fullname).st_mode | stat.S_IWUSR) try: os.path.walk(tmproot, writeable, None) shutil.rmtree(tmproot) except: pass if not tmproot: tmproot = tempfile.mkdtemp(prefix='thg.') atexit.register(cleanup) return tmproot def openhelpcontents(url): 'Open online help, use local CHM file if available' if not url.startswith('http'): fullurl = 'http://tortoisehg.org/manual/2.7/' + url # Use local CHM file if it can be found if os.name == 'nt' and paths.bin_path: chm = os.path.join(paths.bin_path, 'doc', 'TortoiseHg.chm') if os.path.exists(chm): fullurl = (r'mk:@MSITStore:%s::/' % chm) + url openlocalurl(fullurl) return QDesktopServices.openUrl(QUrl(fullurl)) def openlocalurl(path): '''open the given path with the default application takes str, unicode or QString as argument returns True if open was successfull ''' if isinstance(path, str): path = QString(hglib.tounicode(path)) elif isinstance(path, unicode): path = QString(path) if os.name == 'nt' and path.startsWith('\\\\'): # network share, special handling because of qt bug 13359 # see http://bugreports.qt.nokia.com/browse/QTBUG-13359 qurl = QUrl() qurl.setUrl(QDir.toNativeSeparators(path)) else: qurl = QUrl.fromLocalFile(path) return QDesktopServices.openUrl(qurl) def openfiles(repo, files, parent=None): for filename in files: openlocalurl(repo.wjoin(filename)) def editfiles(repo, files, lineno=None, search=None, parent=None): if len(files) == 1: # if editing a single file, open in cwd context of that file filename = files[0].strip() if not filename: return files = [filename] path = repo.wjoin(filename) cwd = os.path.dirname(path) files = [os.path.basename(path)] else: # else edit in cwd context of repo root cwd = repo.root toolpath, args, argsln, argssearch = editor.detecteditor(repo, files) if os.path.basename(toolpath) in ('vi', 'vim', 'hgeditor'): res = QMessageBox.critical(parent, _('No visual editor configured'), _('Please configure a visual editor.')) from tortoisehg.hgqt.settings import SettingsDialog dlg = SettingsDialog(False, focus='tortoisehg.editor') dlg.exec_() return files = [util.shellquote(util.localpath(f)) for f in files] assert len(files) == 1 or lineno == None cmdline = None if search: assert lineno is not None if argssearch: cmdline = ' '.join([toolpath, argssearch]) cmdline = cmdline.replace('$LINENUM', str(lineno)) cmdline = cmdline.replace('$SEARCH', search) elif argsln: cmdline = ' '.join([toolpath, argsln]) cmdline = cmdline.replace('$LINENUM', str(lineno)) elif args: cmdline = ' '.join([toolpath, args]) elif lineno: if argsln: cmdline = ' '.join([toolpath, argsln]) cmdline = cmdline.replace('$LINENUM', str(lineno)) elif args: cmdline = ' '.join([toolpath, args]) else: if args: cmdline = ' '.join([toolpath, args]) if cmdline is None: # editor was not specified by editor-tools configuration, fall # back to older tortoisehg.editor OpenAtLine parsing cmdline = ' '.join([toolpath] + files) # default try: regexp = re.compile('\[([^\]]*)\]') expanded = [] pos = 0 for m in regexp.finditer(toolpath): expanded.append(toolpath[pos:m.start()-1]) phrase = toolpath[m.start()+1:m.end()-1] pos = m.end()+1 if '$LINENUM' in phrase: if lineno is None: # throw away phrase continue phrase = phrase.replace('$LINENUM', str(lineno)) elif '$SEARCH' in phrase: if search is None: # throw away phrase continue phrase = phrase.replace('$SEARCH', search) if '$FILE' in phrase: phrase = phrase.replace('$FILE', files[0]) files = [] expanded.append(phrase) expanded.append(toolpath[pos:]) cmdline = ' '.join(expanded + files) except ValueError, e: # '[' or ']' not found pass except TypeError, e: # variable expansion failed pass shell = not (len(cwd) >= 2 and cwd[0:2] == r'\\') try: if '$FILES' in cmdline: cmdline = cmdline.replace('$FILES', ' '.join(files)) cmdline = util.quotecommand(cmdline) subprocess.Popen(cmdline, shell=shell, creationflags=openflags, stderr=None, stdout=None, stdin=None, cwd=cwd) elif '$FILE' in cmdline: for file in files: cmd = cmdline.replace('$FILE', file) cmd = util.quotecommand(cmd) subprocess.Popen(cmd, shell=shell, creationflags=openflags, stderr=None, stdout=None, stdin=None, cwd=cwd) else: # assume filenames were expanded already cmdline = util.quotecommand(cmdline) subprocess.Popen(cmdline, shell=shell, creationflags=openflags, stderr=None, stdout=None, stdin=None, cwd=cwd) except (OSError, EnvironmentError), e: QMessageBox.warning(parent, _('Editor launch failure'), u'%s : %s' % (hglib.tounicode(cmdline), hglib.tounicode(str(e)))) def savefiles(repo, files, rev, parent=None): for curfile in files: wfile = util.localpath(curfile) wfile, ext = os.path.splitext(os.path.basename(wfile)) extfilter = [_("All files (*)")] if wfile: filename = "%s@%d%s" % (wfile, rev, ext) if ext: extfilter.insert(0, "*%s" % ext) else: filename = "%s@%d" % (ext, rev) result = QFileDialog.getSaveFileName(parent, _("Save file to"), hglib.tounicode(filename), ";;".join(extfilter)) if not result: continue cwd = os.getcwd() try: os.chdir(repo.root) try: commands.cat(repo.ui, repo, curfile, rev=rev, output=hglib.fromunicode(result)) except (util.Abort, IOError), e: QMessageBox.critical(parent, _('Unable to save file'), hglib.tounicode(str(e))) finally: os.chdir(cwd) def openshell(root, reponame, ui=None): if not os.path.exists(root): WarningMsgBox( _('Failed to open path in terminal'), _('"%s" is not a valid directory') % hglib.tounicode(root)) return shell, args = terminal.detectterminal(ui) if shell: cwd = os.getcwd() try: if args: shell = shell + ' ' + util.expandpath(args) shellcmd = shell % {'root': root, 'reponame': reponame} # Unix: QProcess.startDetached(program) cannot parse single-quoted # parameters built using util.shellquote(). # Windows: subprocess.Popen(program, shell=True) cannot spawn # cmd.exe in new window, probably because the initial cmd.exe is # invoked with SW_HIDE. os.chdir(root) fullargs = shlex.split(shellcmd) started = QProcess.startDetached(fullargs[0], fullargs[1:]) finally: os.chdir(cwd) if not started: ErrorMsgBox(_('Failed to open path in terminal'), _('Unable to start the following command:'), shellcmd) else: InfoMsgBox(_('No shell configured'), _('A terminal shell must be configured')) def isdarktheme(palette=None): """True if white-on-black color scheme is preferable""" if not palette: palette = QApplication.palette() return palette.color(QPalette.Base).black() >= 0x80 # _styles maps from ui labels to effects # _effects maps an effect to font style properties. We define a limited # set of _effects, since we convert color effect names to font style # effect programatically. _effects = { 'bold': 'font-weight: bold', 'italic': 'font-style: italic', 'underline': 'text-decoration: underline', } _thgstyles = { # Styles defined by TortoiseHg 'log.branch': 'black #aaffaa_background', 'log.patch': 'black #aaddff_background', 'log.unapplied_patch': 'black #dddddd_background', 'log.tag': 'black #ffffaa_background', 'log.bookmark': 'blue #ffffaa_background', 'log.curbookmark': 'black #ffdd77_background', 'log.modified': 'black #ffddaa_background', 'log.added': 'black #aaffaa_background', 'log.removed': 'black #ffcccc_background', 'status.deleted': 'red bold', 'ui.error': 'red bold #ffcccc_background', 'control': 'black bold #dddddd_background', } thgstylesheet = '* { white-space: pre; font-family: monospace;' \ ' font-size: 9pt; }' tbstylesheet = 'QToolBar { border: 0px }' def configstyles(ui): # extensions may provide more labels and default effects for name, ext in extensions.extensions(): _styles.update(getattr(ext, 'colortable', {})) # tortoisehg defines a few labels and default effects _styles.update(_thgstyles) # allow the user to override for status, cfgeffects in ui.configitems('color'): if '.' not in status: continue cfgeffects = ui.configlist('color', status) _styles[status] = ' '.join(cfgeffects) for status, cfgeffects in ui.configitems('thg-color'): if '.' not in status: continue cfgeffects = ui.configlist('thg-color', status) _styles[status] = ' '.join(cfgeffects) # See http://doc.trolltech.com/4.2/richtext-html-subset.html # and http://www.w3.org/TR/SVG/types.html#ColorKeywords def geteffect(labels): 'map labels like "log.date" to Qt font styles' labels = str(labels) # Could be QString effects = [] # Multiple labels may be requested for l in labels.split(): if not l: continue # Each label may request multiple effects es = _styles.get(l, '') for e in es.split(): if e in _effects: effects.append(_effects[e]) elif e.endswith('_background'): e = e[:-11] if e.startswith('#') or e in QColor.colorNames(): effects.append('background-color: ' + e) elif e.startswith('#') or e in QColor.colorNames(): # Accept any valid QColor effects.append('color: ' + e) return ';'.join(effects) def applyeffects(chars, effects): return '%s' % (effects, chars) def getbgcoloreffect(labels): """Map labels like "log.date" to background color if available Returns QColor object. You may need to check validity by isValid(). """ for l in str(labels).split(): if not l: continue for e in _styles.get(l, '').split(): if e.endswith('_background'): return QColor(e[:-11]) return QColor() NAME_MAP = { 'fg': 'color', 'bg': 'background-color', 'family': 'font-family', 'size': 'font-size', 'weight': 'font-weight', 'space': 'white-space', 'style': 'font-style', 'decoration': 'text-decoration', } def markup(msg, **styles): style = {'white-space': 'pre'} for name, value in styles.items(): if not value: continue if name in NAME_MAP: name = NAME_MAP[name] style[name] = value style = ';'.join(['%s: %s' % t for t in style.items()]) msg = hglib.tounicode(msg) msg = Qt.escape(msg) msg = msg.replace('\n', '
') return u'%s' % (style, msg) def descriptionhtmlizer(ui): """Return a function to mark up ctx.description() as an HTML >>> from mercurial import ui >>> u = ui.ui() >>> htmlize = descriptionhtmlizer(u) >>> htmlize('foo \\n& ') u'foo <bar> \\n& <baz>' changeset hash link: >>> htmlize('foo af50a62e9c20 bar') u'foo af50a62e9c20 bar' >>> htmlize('af50a62e9c2040dcdaf61ba6a6400bb45ab56410') # doctest: +ELLIPSIS u'af...10' http/https links: >>> s = htmlize('foo http://example.com:8000/foo?bar=baz&bax#blah') >>> (s[:63], s[63:]) # doctest: +NORMALIZE_WHITESPACE (u'foo ', u'http://example.com:8000/foo?bar=baz&bax#blah') >>> htmlize('https://example/') u'https://example/' issue links: >>> u.setconfig('tortoisehg', 'issue.regex', r'#(\\d+)\\b') >>> u.setconfig('tortoisehg', 'issue.link', 'http://example/issue/{1}/') >>> htmlize = descriptionhtmlizer(u) >>> htmlize('foo #123') u'foo #123' missing issue.link setting: >>> u.setconfig('tortoisehg', 'issue.link', '') >>> htmlize = descriptionhtmlizer(u) >>> htmlize('foo #123') u'foo #123' too many replacements in issue.link: >>> u.setconfig('tortoisehg', 'issue.link', 'http://example/issue/{1}/{2}') >>> htmlize = descriptionhtmlizer(u) >>> htmlize('foo #123') u'foo #123' invalid regexp in issue.regex: >>> u.setconfig('tortoisehg', 'issue.regex', '(') >>> htmlize = descriptionhtmlizer(u) >>> htmlize('foo #123') u'foo #123' >>> htmlize('http://example/') u'http://example/' """ csmatch = r'(\b[0-9a-f]{12}(?:[0-9a-f]{28})?\b)' httpmatch = r'(\b(http|https)://([-A-Za-z0-9+&@#/%?=~_()|!:,.;]*' \ r'[-A-Za-z0-9+&@#/%=~_()|]))' regexp = r'%s|%s' % (csmatch, httpmatch) bodyre = re.compile(regexp) issuematch = ui.config('tortoisehg', 'issue.regex') issuerepl = ui.config('tortoisehg', 'issue.link') if issuematch and issuerepl: regexp += '|(%s)' % issuematch try: bodyre = re.compile(regexp) except re.error: pass def htmlize(desc): """Mark up ctx.description() [localstr] as an HTML [unicode]""" desc = unicode(Qt.escape(hglib.tounicode(desc))) buf = '' pos = 0 for m in bodyre.finditer(desc): a, b = m.span() if a >= pos: buf += desc[pos:a] pos = b groups = m.groups() if groups[0]: cslink = groups[0] buf += '%s' % (cslink, cslink) if groups[1]: urllink = groups[1] buf += '%s' % (urllink, urllink) if len(groups) > 4 and groups[4]: issue = groups[4] issueparams = groups[4:] try: link = re.sub(r'\{(\d+)\}', lambda m: issueparams[int(m.group(1))], issuerepl) buf += '%s' % (link, issue) except IndexError: buf += issue if pos < len(desc): buf += desc[pos:] return buf return htmlize _iconcache = {} if hasattr(QIcon, 'hasThemeIcon'): # PyQt>=4.7 def _findthemeicon(name): if QIcon.hasThemeIcon(name): return QIcon.fromTheme(name) else: def _findthemeicon(name): pass def _findicon(name): # TODO: icons should be placed at single location before release if os.path.isabs(name): path = name if QFile.exists(path): return QIcon(path) else: for pfx in (':/icons', os.path.join(paths.get_icon_path(), 'svg'), paths.get_icon_path()): for ext in ('svg', 'png', 'ico'): path = '%s/%s.%s' % (pfx, name, ext) if QFile.exists(path): return QIcon(path) return None # http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html _SCALABLE_ICON_PATHS = [(QSize(), 'scalable/actions', '.svg'), (QSize(), 'scalable/apps', '.svg'), (QSize(), 'scalable/status', '.svg'), (QSize(16, 16), '16x16/apps', '.png'), (QSize(22, 22), '22x22/actions', '.png'), (QSize(32, 32), '32x32/actions', '.png'), (QSize(24, 24), '24x24/actions', '.png')] def getallicons(): """Get a sorted, unique list of all available icons""" iconset = set() for size, subdir, sfx in _SCALABLE_ICON_PATHS: path = ':/icons/%s' % (subdir) d = QDir(path) d.setNameFilters(['*%s' % sfx]) for iconname in d.entryList(): iconset.add(unicode(iconname).rsplit('.', 1)[0]) return sorted(iconset) def _findscalableicon(name): """Find icon from qrc by using freedesktop-like icon lookup""" o = QIcon() for size, subdir, sfx in _SCALABLE_ICON_PATHS: path = ':/icons/%s/%s%s' % (subdir, name, sfx) if QFile.exists(path): for mode in (QIcon.Normal, QIcon.Active): o.addFile(path, size, mode) if not o.isNull(): return o def geticon(name): """ Return a QIcon for the specified name. (the given 'name' parameter must *not* provide the extension). This searches for the icon from theme, Qt resource or icons directory, named as 'name.(svg|png|ico)'. """ try: return _iconcache[name] except KeyError: _iconcache[name] = (_findthemeicon(name) or _findscalableicon(name) or _findicon(name) or QIcon(':/icons/fallback.svg')) return _iconcache[name] def getoverlaidicon(base, overlay): """Generate an overlaid icon""" pixmap = base.pixmap(16, 16) painter = QPainter(pixmap) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) painter.drawPixmap(0, 0, overlay.pixmap(16, 16)) del painter return QIcon(pixmap) _pixmapcache = {} def getpixmap(name, width=16, height=16): key = '%s_%sx%s' % (name, width, height) try: return _pixmapcache[key] except KeyError: pixmap = geticon(name).pixmap(width, height) _pixmapcache[key] = pixmap return pixmap def getcheckboxpixmap(state, bgcolor, widget): pix = QPixmap(16,16) painter = QPainter(pix) painter.fillRect(0, 0, 16, 16, bgcolor) option = QStyleOptionButton() style = QApplication.style() option.initFrom(widget) option.rect = style.subElementRect(style.SE_CheckBoxIndicator, option) option.rect.moveTo(1, 1) option.state |= state style.drawPrimitive(style.PE_IndicatorCheckBox, option, painter) return pix class ThgFont(QObject): changed = pyqtSignal(QFont) def __init__(self, name): QObject.__init__(self) self.myfont = QFont() self.myfont.fromString(name) def font(self): return self.myfont def setFont(self, f): self.myfont = f self.changed.emit(f) _fontdefaults = { 'fontcomment': 'monospace,10', 'fontdiff': 'monospace,10', 'fontlist': 'sans,9', 'fontlog': 'monospace,10', 'fontoutputlog': 'sans,8' } if sys.platform == 'darwin': _fontdefaults['fontoutputlog'] = 'sans,10' _fontcache = {} def initfontcache(ui): for name in _fontdefaults: fname = ui.config('tortoisehg', name, _fontdefaults[name]) _fontcache[name] = ThgFont(hglib.tounicode(fname)) def getfont(name): assert name in _fontdefaults return _fontcache[name] def CommonMsgBox(icon, title, main, text='', buttons=QMessageBox.Ok, labels=[], parent=None, defaultbutton=None): msg = QMessageBox(parent) msg.setIcon(icon) msg.setWindowTitle(title) msg.setStandardButtons(buttons) for button_id, label in labels: msg.setButtonText(button_id, label) if defaultbutton: msg.setDefaultButton(defaultbutton) msg.setText('%s' % main) info = '' for line in text.split('\n'): info += '%s
' % line msg.setInformativeText(info) return msg.exec_() def InfoMsgBox(*args, **kargs): return CommonMsgBox(QMessageBox.Information, *args, **kargs) def WarningMsgBox(*args, **kargs): return CommonMsgBox(QMessageBox.Warning, *args, **kargs) def ErrorMsgBox(*args, **kargs): return CommonMsgBox(QMessageBox.Critical, *args, **kargs) def QuestionMsgBox(*args, **kargs): btn = QMessageBox.Yes | QMessageBox.No res = CommonMsgBox(QMessageBox.Question, buttons=btn, *args, **kargs) return res == QMessageBox.Yes class CustomPrompt(QMessageBox): def __init__(self, title, message, parent, choices, default=None, esc=None, files=None): QMessageBox.__init__(self, parent) self.setWindowTitle(hglib.tounicode(title)) self.setText(hglib.tounicode(message)) if files: self.setDetailedText(hglib.tounicode('\n'.join(files))) self.hotkeys = {} for i, s in enumerate(choices): btn = self.addButton(s, QMessageBox.AcceptRole) try: char = s[s.index('&')+1].lower() self.hotkeys[char] = btn except (ValueError, IndexError): pass if default == i: self.setDefaultButton(btn) if esc == i: self.setEscapeButton(btn) def run(self): return self.exec_() def keyPressEvent(self, event): for k, btn in self.hotkeys.iteritems(): if event.text() == k: btn.clicked.emit(False) super(CustomPrompt, self).keyPressEvent(event) class ChoicePrompt(QDialog): def __init__(self, title, message, parent, choices, default=None, esc=None, files=None): QDialog.__init__(self, parent) self.setWindowIcon(geticon('thg_logo')) self.setWindowTitle(hglib.tounicode(title)) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.box = QHBoxLayout() self.vbox = QVBoxLayout() self.vbox.setSpacing(8) self.message_lbl = QLabel() self.message_lbl.setText(message) self.vbox.addWidget(self.message_lbl) self.choice_combo = combo = QComboBox() self.choices = choices combo.addItems([hglib.tounicode(item) for item in choices]) if default: try: combo.setCurrentIndex(choices.index(default)) except: # Ignore a missing default value pass self.vbox.addWidget(combo) self.box.addLayout(self.vbox) vbox = QVBoxLayout() self.ok = QPushButton('&OK') self.ok.clicked.connect(self.accept) vbox.addWidget(self.ok) self.cancel = QPushButton('&Cancel') self.cancel.clicked.connect(self.reject) vbox.addWidget(self.cancel) vbox.addStretch() self.box.addLayout(vbox) self.setLayout(self.box) def run(self): if self.exec_(): return self.choices[self.choice_combo.currentIndex()] return None def allowCaseChangingInput(combo): """Allow case-changing input of known combobox item QComboBox performs case-insensitive inline completion by default. It's all right, but sadly it implies case-insensitive check for duplicates, i.e. you can no longer enter "Foo" if the combobox contains "foo". For details, read QComboBoxPrivate::_q_editingFinished() and matchFlags() of src/gui/widgets/qcombobox.cpp. """ assert isinstance(combo, QComboBox) and combo.isEditable() combo.completer().setCaseSensitivity(Qt.CaseSensitive) class PMButton(QPushButton): """Toggle button with plus/minus icon images""" def __init__(self, expanded=True, parent=None): QPushButton.__init__(self, parent) size = QSize(11, 11) self.setIconSize(size) self.setMaximumSize(size) self.setFlat(True) self.setAutoDefault(False) self.plus = geticon('expander-open') self.minus = geticon('expander-close') icon = expanded and self.minus or self.plus self.setIcon(icon) self.clicked.connect(self._toggle_icon) @pyqtSlot() def _toggle_icon(self): icon = self.is_expanded() and self.plus or self.minus self.setIcon(icon) def set_expanded(self, state=True): icon = state and self.minus or self.plus self.setIcon(icon) def set_collapsed(self, state=True): icon = state and self.plus or self.minus self.setIcon(icon) def is_expanded(self): return self.icon().serialNumber() == self.minus.serialNumber() def is_collapsed(self): return not self.is_expanded() class ClickableLabel(QLabel): clicked = pyqtSignal() def __init__(self, label, parent=None): QLabel.__init__(self, parent) self.setText(label) def mouseReleaseEvent(self, event): self.clicked.emit() class ExpanderLabel(QWidget): expanded = pyqtSignal(bool) def __init__(self, label, expanded=True, stretch=True, parent=None): QWidget.__init__(self, parent) box = QHBoxLayout() box.setSpacing(4) box.setContentsMargins(*(0,)*4) self.button = PMButton(expanded, self) self.button.clicked.connect(self.pm_clicked) box.addWidget(self.button) self.label = ClickableLabel(label, self) self.label.clicked.connect(self.button.click) box.addWidget(self.label) if not stretch: box.addStretch(0) self.setLayout(box) def pm_clicked(self): self.expanded.emit(self.button.is_expanded()) def set_expanded(self, state=True): if not self.button.is_expanded() == state: self.button.set_expanded(state) self.expanded.emit(state) def is_expanded(self): return self.button.is_expanded() class StatusLabel(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) box = QHBoxLayout() box.setContentsMargins(*(0,)*4) self.status_icon = QLabel() self.status_icon.setMaximumSize(16, 16) self.status_icon.setAlignment(Qt.AlignCenter) box.addWidget(self.status_icon) self.status_text = QLabel() self.status_text.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) box.addWidget(self.status_text) box.addStretch(0) self.setLayout(box) def set_status(self, text, icon=None): self.set_text(text) self.set_icon(icon) def clear_status(self): self.clear_text() self.clear_icon() def set_text(self, text=''): if text is None: text = '' self.status_text.setText(text) def clear_text(self): self.set_text() def set_icon(self, icon=None): if icon is None: self.clear_icon() else: if isinstance(icon, bool): icon = geticon(icon and 'thg-success' or 'thg-error') elif isinstance(icon, basestring): icon = geticon(icon) elif not isinstance(icon, QIcon): raise TypeError, '%s: bool, str or QIcon' % type(icon) self.status_icon.setShown(True) self.status_icon.setPixmap(icon.pixmap(16, 16)) def clear_icon(self): self.status_icon.setHidden(True) class LabeledSeparator(QWidget): def __init__(self, label=None, parent=None): QWidget.__init__(self, parent) box = QHBoxLayout() box.setContentsMargins(*(0,)*4) if label: if isinstance(label, basestring): label = QLabel(label) box.addWidget(label) sep = QFrame() sep.setFrameShadow(QFrame.Sunken) sep.setFrameShape(QFrame.HLine) box.addWidget(sep, 1, Qt.AlignVCenter) self.setLayout(box) # Strings and regexes used to convert hashes and subrepo paths into links _hashregex = re.compile(r'\b[0-9a-fA-F]{12,}') # Currently converting subrepo paths into links only works in English _subrepoindicatorpattern = hglib.tounicode(hggettext('(in subrepo %s)') + '\n') def _linkifyHash(message, subrepo=''): if subrepo: p = 'repo:%s?' % subrepo else: p = 'cset:' replaceexpr = lambda m: '%s' % (p + m.group(0), m.group(0)) return _hashregex.sub(replaceexpr, message) def _linkifySubrepoRef(message, subrepo, hash=''): if hash: hash = '?' + hash subrepolink = '%s' % (subrepo, hash, subrepo) subrepoindicator = _subrepoindicatorpattern % subrepo linkifiedsubrepoindicator = _subrepoindicatorpattern % subrepolink message = message.replace(subrepoindicator, linkifiedsubrepoindicator) return message def linkifyMessage(message, subrepo=None): r"""Convert revision id hashes and subrepo paths in messages into links >>> linkifyMessage('abort: 0123456789ab!\nhint: foo\n') u'abort: 0123456789ab!
hint: foo
' >>> linkifyMessage('abort: foo (in subrepo bar)\n', subrepo='bar') u'abort: foo (in subrepo bar)
' >>> linkifyMessage('abort: 0123456789ab! (in subrepo bar)\nhint: foo\n', ... subrepo='bar') #doctest: +NORMALIZE_WHITESPACE u'abort: 0123456789ab! (in subrepo bar)
hint: foo
' subrepo name containing regexp backreference, \g: >>> linkifyMessage('abort: 0123456789ab! (in subrepo foo\\goo)\n', ... subrepo='foo\\goo') #doctest: +NORMALIZE_WHITESPACE u'abort: 0123456789ab! (in subrepo foo\\goo)
' """ message = unicode(message) message = _linkifyHash(message, subrepo) if subrepo: hash = '' m = _hashregex.search(message) if m: hash = m.group(0) message = _linkifySubrepoRef(message, subrepo, hash) return message.replace('\n', '
') class InfoBar(QFrame): """Non-modal confirmation/alert (like web flash or Chrome's InfoBar) Layout:: |widgets ... |right widgets ...|x| """ finished = pyqtSignal(int) # mimic QDialog linkActivated = pyqtSignal(unicode) # type of InfoBar (the number denotes its priority) INFO = 1 ERROR = 2 CONFIRM = 3 infobartype = INFO _colormap = { INFO: '#e7f9e0', ERROR: '#f9d8d8', CONFIRM: '#fae9b3', } def __init__(self, parent=None): super(InfoBar, self).__init__(parent, frameShape=QFrame.StyledPanel, frameShadow=QFrame.Plain) self.setAutoFillBackground(True) p = self.palette() p.setColor(QPalette.Window, QColor(self._colormap[self.infobartype])) p.setColor(QPalette.WindowText, QColor("black")) self.setPalette(p) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 2, 2, 2) self.layout().addStretch() self._closebutton = QPushButton(self, flat=True, autoDefault=False, icon=self.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) self._closebutton.clicked.connect(self.close) self.layout().addWidget(self._closebutton) def addWidget(self, w, stretch=0): self.layout().insertWidget(self.layout().count() - 2, w, stretch) def addRightWidget(self, w): self.layout().insertWidget(self.layout().count() - 1, w) def closeEvent(self, event): if self.isVisible(): self.finished.emit(0) super(InfoBar, self).closeEvent(event) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.close() super(InfoBar, self).keyPressEvent(event) def heightForWidth(self, width): # loosely based on the internal strategy of QBoxLayout if self.layout().hasHeightForWidth(): return super(InfoBar, self).heightForWidth(width) else: return self.sizeHint().height() class StatusInfoBar(InfoBar): """Show status message""" def __init__(self, message, parent=None): super(StatusInfoBar, self).__init__(parent) self._msglabel = QLabel(message, self, wordWrap=True, textInteractionFlags=Qt.TextSelectableByMouse \ | Qt.LinksAccessibleByMouse) self._msglabel.linkActivated.connect(self.linkActivated) self.addWidget(self._msglabel, stretch=1) class CommandErrorInfoBar(InfoBar): """Show command execution failure (with link to open log window)""" infobartype = InfoBar.ERROR def __init__(self, message, parent=None): super(CommandErrorInfoBar, self).__init__(parent) self._msglabel = QLabel(message, self, wordWrap=True, textInteractionFlags=Qt.TextSelectableByMouse \ | Qt.LinksAccessibleByMouse) self._msglabel.linkActivated.connect(self.linkActivated) self.addWidget(self._msglabel, stretch=1) self._loglabel = QLabel('%s' % _('Show Log')) self._loglabel.linkActivated.connect(self.linkActivated) self.addRightWidget(self._loglabel) class ConfirmInfoBar(InfoBar): """Show confirmation message with accept/reject buttons""" accepted = pyqtSignal() rejected = pyqtSignal() infobartype = InfoBar.CONFIRM def __init__(self, message, parent=None): super(ConfirmInfoBar, self).__init__(parent) # no wordWrap=True and stretch=1, which inserts unwanted space # between _msglabel and _buttons. self._msglabel = QLabel(message, self, textInteractionFlags=Qt.TextSelectableByMouse \ | Qt.LinksAccessibleByMouse) self._msglabel.linkActivated.connect(self.linkActivated) self.addWidget(self._msglabel) self._buttons = QDialogButtonBox(self) self._buttons.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.acceptButton = self._buttons.addButton(QDialogButtonBox.Ok) self.rejectButton = self._buttons.addButton(QDialogButtonBox.Cancel) self._buttons.accepted.connect(self._accept) self._buttons.rejected.connect(self._reject) self.addWidget(self._buttons) # so that acceptButton gets focus by default self.setFocusProxy(self._buttons) def closeEvent(self, event): if self.isVisible(): self.finished.emit(1) self.rejected.emit() self.hide() # avoid double emission of finished signal super(ConfirmInfoBar, self).closeEvent(event) @pyqtSlot() def _accept(self): self.finished.emit(0) self.accepted.emit() self.hide() self.close() @pyqtSlot() def _reject(self): self.finished.emit(1) self.rejected.emit() self.hide() self.close() class WidgetGroups(object): """ Support for bulk-updating properties of Qt widgets """ def __init__(self): object.__init__(self) self.clear(all=True) ### Public Methods ### def add(self, widget, group='default'): if group not in self.groups: self.groups[group] = [] widgets = self.groups[group] if widget not in widgets: widgets.append(widget) def remove(self, widget, group='default'): if group not in self.groups: return widgets = self.groups[group] if widget in widgets: widgets.remove(widget) def clear(self, group='default', all=True): if all: self.groups = {} else: del self.groups[group] def set_prop(self, prop, value, group='default', cond=None): if group not in self.groups: return widgets = self.groups[group] if callable(cond): widgets = [w for w in widgets if cond(w)] for widget in widgets: getattr(widget, prop)(value) def set_visible(self, *args, **kargs): self.set_prop('setVisible', *args, **kargs) def set_enable(self, *args, **kargs): self.set_prop('setEnabled', *args, **kargs) class DialogKeeper(QObject): """Manage non-blocking dialogs identified by creation parameters Example "open single dialog per type": >>> mainwin = QWidget() >>> dialogs = DialogKeeper(lambda self, cls: cls(self), parent=mainwin) >>> dlg1 = dialogs.open(QDialog) >>> dlg1.parent() is mainwin True >>> dlg2 = dialogs.open(QDialog) >>> dlg1 is dlg2 True >>> dialogs.count() 1 closed dialog will be disowned: >>> dlg1.reject() >>> dlg1.parent() is None True >>> dialogs.count() 0 and recreates as necessary: >>> dlg3 = dialogs.open(QDialog) >>> dlg1 is dlg3 False creates new dialog of the same type: >>> dlg4 = dialogs.openNew(QDialog) >>> dlg3 is dlg4 False >>> dialogs.count() 2 and the last dialog is preferred: >>> dialogs.open(QDialog) is dlg4 True >>> dlg4.reject() >>> dialogs.count() 1 >>> dialogs.open(QDialog) is dlg3 True The following example is not recommended because it creates reference cycles and makes hard to garbage-collect:: self._dialogs = DialogKeeper(self._createDialog) self._dialogs = DialogKeeper(lambda *args: Foo(self)) When to delete reference: If a dialog is not referenced, and if accept(), reject() and done() are not overridden, it could be garbage-collected during finished signal and lead to hard crash:: #0 isSignalConnected (signal_index=..., this=0x0) #1 QMetaObject::activate (...) #2 ... in sipQDialog::done (this=0x1d655b0, ...) #3 ... in sipQDialog::reject (this=0x1d655b0) #4 ... in QDialog::closeEvent (this=this@entry=0x1d655b0, ...) To avoid crash, a finished dialog is referenced explicitly by DialogKeeper until next event processing. >>> dialogs = DialogKeeper(QDialog) >>> dlgref = weakref.ref(dialogs.open()) >>> dialogs.count() 1 >>> esckeyev = QKeyEvent(QEvent.KeyPress, Qt.Key_Escape, Qt.NoModifier) >>> QApplication.postEvent(dlgref(), esckeyev) # close without incref >>> QApplication.processEvents() # should not crash >>> dialogs.count() 0 >>> dlgref() is None # nobody should have reference True """ def __init__(self, createdlg, genkey=None, parent=None): super(DialogKeeper, self).__init__(parent) self._createdlg = createdlg self._genkey = genkey or DialogKeeper._defaultgenkey self._keytodlgs = {} # key: [dlg, ...] self._dlgtokey = {} # dlg: key self._garbagedlgs = [] self._emptygarbagelater = QTimer(self, interval=0, singleShot=True) self._emptygarbagelater.timeout.connect(self._emptygarbage) def open(self, *args, **kwargs): """Create new dialog or reactivate existing dialog""" dlg = self._preparedlg(self._genkey(self.parent(), *args, **kwargs), args, kwargs) dlg.show() dlg.raise_() dlg.activateWindow() return dlg def openNew(self, *args, **kwargs): """Create new dialog even if there exists the specified one""" dlg = self._populatedlg(self._genkey(self.parent(), *args, **kwargs), args, kwargs) dlg.show() dlg.raise_() dlg.activateWindow() return dlg def _preparedlg(self, key, args, kwargs): if key in self._keytodlgs: assert len(self._keytodlgs[key]) > 0 return self._keytodlgs[key][-1] # prefer latest else: return self._populatedlg(key, args, kwargs) def _populatedlg(self, key, args, kwargs): dlg = self._createdlg(self.parent(), *args, **kwargs) if key not in self._keytodlgs: self._keytodlgs[key] = [] self._keytodlgs[key].append(dlg) self._dlgtokey[dlg] = key dlg.finished.connect(self._forgetdlg) return dlg #@pyqtSlot() def _forgetdlg(self): dlg = self.sender() dlg.finished.disconnect(self._forgetdlg) if dlg.parent() is self.parent(): dlg.setParent(None) # assist gc key = self._dlgtokey.pop(dlg) self._keytodlgs[key].remove(dlg) if not self._keytodlgs[key]: del self._keytodlgs[key] # avoid deletion inside finished signal self._garbagedlgs.append(dlg) self._emptygarbagelater.start() @pyqtSlot() def _emptygarbage(self): del self._garbagedlgs[:] def count(self): assert len(self._dlgtokey) == sum(len(dlgs) for dlgs in self._keytodlgs.itervalues()) return len(self._dlgtokey) @staticmethod def _defaultgenkey(_parent, *args, **_kwargs): return args class TaskWidget(object): def canswitch(self): """Return True if the widget allows to switch away from it""" return True def canExit(self): return True class DemandWidget(QWidget): 'Create a widget the first time it is shown' def __init__(self, createfuncname, createinst, parent=None): super(DemandWidget, self).__init__(parent) # We store a reference to the create function name to avoid having a # hard reference to the bound function, which prevents it being # disposed. Weak references to bound functions don't work. self._createfuncname = createfuncname self._createinst = weakref.ref(createinst) self._widget = None vbox = QVBoxLayout() vbox.setContentsMargins(*(0,)*4) self.setLayout(vbox) def showEvent(self, event): """create the widget if necessary""" self.get() super(DemandWidget, self).showEvent(event) def forward(self, funcname, *args, **opts): if self._widget: return getattr(self._widget, funcname)(*args, **opts) return None def get(self): """Returns the stored widget""" if self._widget is None: func = getattr(self._createinst(), self._createfuncname, None) self._widget = func() self.layout().addWidget(self._widget) return self._widget def canswitch(self): """Return True if the widget allows to switch away from it""" if self._widget is None: return True return self._widget.canswitch() def canExit(self): if self._widget is None: return True return self._widget.canExit() def __getattr__(self, name): return getattr(self._widget, name) class Spacer(QWidget): """Spacer to separate controls in a toolbar""" def __init__(self, width, height, parent=None): QWidget.__init__(self, parent) self.width = width self.height = height def sizeHint(self): return QSize(self.width, self.height) def _configuredusername(ui): # need to check the existence before calling ui.username(); otherwise it # may fall back to the system default. if (not os.environ.get('HGUSER') and not ui.config('ui', 'username') and not os.environ.get('EMAIL')): return None try: return ui.username() except error.Abort: return None def getCurrentUsername(widget, repo, opts=None): if opts: # 1. Override has highest priority user = opts.get('user') if user: return user # 2. Read from repository user = _configuredusername(repo.ui) if user: return user # 3. Get a username from the user QMessageBox.information(widget, _('Please enter a username'), _('You must identify yourself to Mercurial'), QMessageBox.Ok) from tortoisehg.hgqt.settings import SettingsDialog dlg = SettingsDialog(False, focus='ui.username') dlg.exec_() repo.invalidateui() return _configuredusername(repo.ui) class _EncodingSafeInputDialog(QInputDialog): def accept(self): try: hglib.fromunicode(self.textValue()) return super(_EncodingSafeInputDialog, self).accept() except UnicodeEncodeError: WarningMsgBox(_('Text Translation Failure'), _('Unable to translate input to local encoding.'), parent=self) def getTextInput(parent, title, label, mode=QLineEdit.Normal, text='', flags=Qt.WindowFlags()): flags |= (Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) dlg = _EncodingSafeInputDialog(parent, flags) dlg.setWindowTitle(title) dlg.setLabelText(label) dlg.setTextValue(text) dlg.setTextEchoMode(mode) r = dlg.exec_() dlg.setParent(None) # so that garbage collected return r and dlg.textValue() or '', bool(r) def keysequence(o): """Create QKeySequence from string or QKeySequence""" if isinstance(o, (QKeySequence, QKeySequence.StandardKey)): return o try: return getattr(QKeySequence, str(o)) # standard key except AttributeError: return QKeySequence(o) def modifiedkeysequence(o, modifier): """Create QKeySequence of modifier key prepended""" origseq = QKeySequence(keysequence(o)) return QKeySequence('%s+%s' % (modifier, origseq.toString())) def newshortcutsforstdkey(key, *args, **kwargs): """Create [QShortcut,...] for all key bindings of the given StandardKey""" return [QShortcut(keyseq, *args, **kwargs) for keyseq in QKeySequence.keyBindings(key)] class PaletteSwitcher(object): """ Class that can be used to enable a predefined, alterantive background color for a widget This is normally used to change the color of widgets when they display some "filtered" content which is a subset of the actual widget contents. The alternative background color is fixed, and depends on the original background color (dark and light backgrounds use a different alternative color). The alterenative color cannot be changed because the idea is to set a consistent "filter" style for all widgets. An instance of this class must be added as a property of the widget whose background we want to change. The constructor takes the "target widget" as its only parameter. In order to enable or disable the background change, simply call the enablefilterpalette() method. """ def __init__(self, targetwidget): self._targetwref = weakref.ref(targetwidget) # avoid circular ref self._defaultpalette = targetwidget.palette() if not isdarktheme(self._defaultpalette): filterbgcolor = QColor('#FFFFB7') else: filterbgcolor = QColor('darkgrey') self._filterpalette = QPalette() self._filterpalette.setColor(QPalette.Base, filterbgcolor) def enablefilterpalette(self, enabled=False): targetwidget = self._targetwref() if not targetwidget: return if enabled: pl = self._filterpalette else: pl = self._defaultpalette targetwidget.setPalette(pl) tortoisehg-2.10/tortoisehg/hgqt/serve.py0000644000076400007640000001752112231647662017540 0ustar stevesteve# serve.py - TortoiseHg dialog to start web server # # Copyright 2010 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os, tempfile from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util, error from tortoisehg.util import paths, wconfig, hglib from tortoisehg.hgqt import cmdcore, cmdui, qtlib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt.serve_ui import Ui_ServeDialog from tortoisehg.hgqt.webconf import WebconfForm class ServeDialog(QDialog): """Dialog for serving repositories via web""" def __init__(self, webconf, parent=None): super(ServeDialog, self).__init__(parent) self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint) & ~Qt.WindowContextHelpButtonHint) # TODO: choose appropriate icon self.setWindowIcon(qtlib.geticon('proxy')) self._qui = Ui_ServeDialog() self._qui.setupUi(self) self._initwebconf(webconf) self._initcmd() self._initactions() self._updateform() def _initcmd(self): # TODO: forget old logs? self._log_edit = cmdui.LogWidget(self) self._qui.details_tabs.addTab(self._log_edit, _('Log')) self._agent = cmdcore.CmdAgent(self) self._agent.outputReceived.connect(self._log_edit.appendLog) self._agent.busyChanged.connect(self._updateform) def _initwebconf(self, webconf): self._webconf_form = WebconfForm(webconf=webconf, parent=self) self._qui.details_tabs.addTab(self._webconf_form, _('Repositories')) def _initactions(self): self._qui.start_button.clicked.connect(self.start) self._qui.stop_button.clicked.connect(self.stop) @pyqtSlot() def _updateform(self): """update form availability and status text""" self._updatestatus() self._qui.start_button.setEnabled(not self.isstarted()) self._qui.stop_button.setEnabled(self.isstarted()) self._qui.settings_button.setEnabled(not self.isstarted()) self._qui.port_edit.setEnabled(not self.isstarted()) self._webconf_form.setEnabled(not self.isstarted()) def _updatestatus(self): if self.isstarted(): # TODO: escape special chars link = '%s' % (self.rooturl, self.rooturl) msg = _('Running at %s') % link else: msg = _('Stopped') self._qui.status_edit.setText(msg) @pyqtSlot() def start(self): """Start web server""" if self.isstarted(): return self._agent.runCommand(map(hglib.tounicode, self._cmdargs()), worker='proc') def _cmdargs(self): """Build command args to run server""" a = ['serve', '--port', str(self.port), '--debug'] if self._singlerepo: a += ['-R', self._singlerepo] else: a += ['--web-conf', self._tempwebconf()] return a def _tempwebconf(self): """Save current webconf to temporary file; return its path""" if not hasattr(self._webconf, 'write'): return self._webconf.path fd, fname = tempfile.mkstemp(prefix='webconf_', dir=qtlib.gettempdir()) f = os.fdopen(fd, 'w') try: self._webconf.write(f) return fname finally: f.close() @property def _webconf(self): """Selected webconf object""" return self._webconf_form.webconf @property def _singlerepo(self): """Return repository path if serving single repository""" # NOTE: we cannot use web-conf to serve single repository at '/' path if len(self._webconf['paths']) != 1: return path = self._webconf.get('paths', '/') if path and '*' not in path: # exactly a single repo (no wildcard) return path @pyqtSlot() def stop(self): """Stop web server""" self._agent.abortCommands() def reject(self): self.stop() super(ServeDialog, self).reject() def isstarted(self): """Is the web server running?""" return self._agent.isBusy() @property def rooturl(self): """Returns the root URL of the web server""" # TODO: scheme, hostname ? return 'http://localhost:%d' % self.port @property def port(self): """Port number of the web server""" return int(self._qui.port_edit.value()) def setport(self, port): self._qui.port_edit.setValue(port) def keyPressEvent(self, event): if self.isstarted() and event.key() == Qt.Key_Escape: self.stop() return return super(ServeDialog, self).keyPressEvent(event) def closeEvent(self, event): if self.isstarted(): self._minimizetotray() event.ignore() return return super(ServeDialog, self).closeEvent(event) @util.propertycache def _trayicon(self): icon = QSystemTrayIcon(self.windowIcon(), parent=self) icon.activated.connect(self._restorefromtray) icon.setToolTip(self.windowTitle()) # TODO: context menu return icon # TODO: minimize to tray by minimize button @pyqtSlot() def _minimizetotray(self): self._trayicon.show() self._trayicon.showMessage(_('TortoiseHg Web Server'), _('Running at %s') % self.rooturl) self.hide() @pyqtSlot() def _restorefromtray(self): self._trayicon.hide() self.show() @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings settings.SettingsDialog(parent=self, focus='web.name').exec_() def _asconfigliststr(value): r""" >>> _asconfigliststr('foo') 'foo' >>> _asconfigliststr('foo bar') '"foo bar"' >>> _asconfigliststr('foo,bar') '"foo,bar"' >>> _asconfigliststr('foo "bar"') '"foo \\"bar\\""' """ # ui.configlist() uses isspace(), which is locale-dependent if util.any(c.isspace() or c == ',' for c in value): return '"' + value.replace('"', '\\"') + '"' else: return value def _readconfig(ui, repopath, webconfpath): """Create new ui and webconf object and read appropriate files""" lui = ui.copy() if webconfpath: lui.readconfig(webconfpath) # TODO: handle file not found c = wconfig.readfile(webconfpath) c.path = os.path.abspath(webconfpath) return lui, c elif repopath: # imitate webconf for single repo lui.readconfig(os.path.join(repopath, '.hg', 'hgrc'), repopath) c = wconfig.config() try: if not os.path.exists(os.path.join(repopath, '.hgsub')): # no _asconfigliststr(repopath) for now, because ServeDialog # cannot parse it as a list in single-repo mode. c.set('paths', '/', repopath) else: # since hg 8cbb59124e67, path entry is parsed as a list base = lui.config('web', 'name') or os.path.basename(repopath) c.set('paths', base, _asconfigliststr(os.path.join(repopath, '**'))) except (EnvironmentError, error.Abort, error.RepoError): c.set('paths', '/', repopath) return lui, c else: return lui, None def run(ui, *pats, **opts): repopath = opts.get('root') or paths.find_root() webconfpath = opts.get('web_conf') or opts.get('webdir_conf') lui, webconf = _readconfig(ui, repopath, webconfpath) dlg = ServeDialog(webconf=webconf) try: dlg.setport(int(lui.config('web', 'port', '8000'))) except ValueError: pass if repopath or webconfpath: dlg.start() return dlg tortoisehg-2.10/tortoisehg/hgqt/qdelete.py0000644000076400007640000000256512231647662020041 0ustar stevesteve# qdelete.py - QDelete dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ class QDeleteDialog(QDialog): def __init__(self, patches, parent): super(QDeleteDialog, self).__init__(parent) self.setWindowTitle(_('Delete Patches')) self.setWindowIcon(qtlib.geticon('hg-qdelete')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setLayout(QVBoxLayout()) msg = _('Remove patches from queue?') patchesu = u'
  • '.join(patches) lbl = QLabel(u'%s
    • %s
    ' % (msg, patchesu)) self.layout().addWidget(lbl) self._keepchk = QCheckBox(_('Keep patch files')) self._keepchk.setChecked(True) self.layout().addWidget(self._keepchk) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Ok|BB.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) def options(self): return {'keep': self._keepchk.isChecked()} tortoisehg-2.10/tortoisehg/hgqt/lfprompt.py0000644000076400007640000000447412110205646020246 0ustar stevesteve# lfprompt.py - prompt to add large files # # Copyright 2011 Fog Creek Software # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os from mercurial import match from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ class LfilesPrompt(qtlib.CustomPrompt): def __init__(self, parent, files=None): qtlib.CustomPrompt.__init__(self, _('Confirm Add'), _('Some of the files that you have selected are of a size ' 'over 10 MB. You may make more efficient use of disk space ' 'by adding these files as largefiles, which will store only the ' 'most recent revision of each file in your local repository, ' 'with older revisions available on the server. Do you wish ' 'to add these files as largefiles?'), parent, (_('Add as &Largefiles'), _('Add as &Normal Files'), _('Cancel')), 0, 2, files) def promptForLfiles(parent, ui, repo, files): lfiles = [] uself = 'largefiles' in repo.extensions() section = 'largefiles' try: minsize = int(ui.config(section, 'minsize', default='10')) except ValueError: minsize = 10 patterns = ui.config(section, 'patterns', default=()) if patterns: patterns = patterns.split(' ') matcher = match.match(repo.root, '', list(patterns)) else: matcher = None for wfile in files: if matcher and matcher(wfile): # patterns have always precedence over size lfiles.append(wfile) else: # check for minimal size filesize = os.path.getsize(repo.wjoin(wfile)) if filesize > minsize*1024*1024: lfiles.append(wfile) if lfiles: ret = LfilesPrompt(parent, files).run() if ret == 0: # add as largefiles/bfiles for lfile in lfiles: files.remove(lfile) elif ret == 1: # add as normal files lfiles = [] elif ret == 2: return None return files, lfiles tortoisehg-2.10/tortoisehg/hgqt/thread.py0000644000076400007640000003431012231647662017656 0ustar stevesteve# thread.py - A separate thread to run Mercurial commands # # Copyright 2009 Steve Borho # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import Queue import urllib2, urllib import socket import errno from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util, error, subrepo from mercurial import ui as uimod from tortoisehg.util import thread2, hglib from tortoisehg.hgqt.i18n import _, localgettext from tortoisehg.hgqt import qtlib local = localgettext() class DataWrapper(object): def __init__(self, data): self.data = data class UiSignal(QObject): writeSignal = pyqtSignal(QString, QString) progressSignal = pyqtSignal(QString, object, QString, QString, object) interactSignal = pyqtSignal(DataWrapper) def __init__(self, responseq): QObject.__init__(self) self.responseq = responseq def write(self, *args, **opts): msg = hglib.tounicode(''.join(args)) label = hglib.tounicode(opts.get('label', '')) self.writeSignal.emit(msg, label) def write_err(self, *args, **opts): msg = hglib.tounicode(''.join(args)) label = hglib.tounicode(opts.get('label', 'ui.error')) self.writeSignal.emit(msg, label) def prompt(self, msg, default): try: r = self._waitresponse(msg, False, None, None) if r is None: raise EOFError if not r: return default return r except EOFError: raise util.Abort(local._('response expected')) def promptchoice(self, msg, choices, default): try: r = self._waitresponse(msg, False, choices, default) if r is None: raise EOFError return r except EOFError: raise util.Abort(local._('response expected')) def getpass(self, prompt, default): r = self._waitresponse(prompt, True, None, default) if r is None: raise util.Abort(local._('response expected')) return r def _waitresponse(self, msg, password, choices, default): """Request interaction with GUI and wait response from it""" data = DataWrapper((msg, password, choices, default)) self.interactSignal.emit(data) # await response return self.responseq.get(True) def progress(self, topic, pos, item, unit, total): topic = hglib.tounicode(topic or '') item = hglib.tounicode(item or '') unit = hglib.tounicode(unit or '') self.progressSignal.emit(topic, pos, item, unit, total) class QtUi(uimod.ui): def __init__(self, src=None, responseq=None): super(QtUi, self).__init__(src) if src: self.sig = src.sig else: self.sig = UiSignal(responseq) self.setconfig('ui', 'interactive', 'on') self.setconfig('progress', 'disable', 'True') def write(self, *args, **opts): if self._buffers: self._buffers[-1].extend([str(a) for a in args]) else: self.sig.write(*args, **opts) def write_err(self, *args, **opts): self.sig.write_err(*args, **opts) def label(self, msg, label): return msg def flush(self): pass def prompt(self, msg, default='y'): if not self.interactive(): return default return self.sig.prompt(msg, default) def promptchoice(self, prompt, default=0): if not self.interactive(): return default parts = prompt.split('$$') msg = parts[0].rstrip(' ') choices = [p.strip(' ') for p in parts[1:]] return self.sig.promptchoice(msg, choices, default) def getpass(self, prompt=None, default=None): return self.sig.getpass(prompt or _('password: '), default) def progress(self, topic, pos, item='', unit='', total=None): return self.sig.progress(topic, pos, item, unit, total) class CmdThread(QThread): """Run an Mercurial command in a background thread, implies output is being sent to a rendered text buffer interactively and requests for feedback from Mercurial can be handled by the user via dialog windows. """ # (msg=str, label=str) outputReceived = pyqtSignal(QString, QString) # (topic=str, pos=int, item=str, unit=str, total=int) # pos and total are emitted as object, since they may be None progressReceived = pyqtSignal(QString, object, QString, QString, object) # result: -1 - command is incomplete, possibly exited with exception # 0 - command is finished successfully # others - return code of command commandFinished = pyqtSignal(int) def __init__(self, cmdline, parent=None): super(CmdThread, self).__init__(parent) self.cmdline = cmdline self.ret = -1 self.responseq = Queue.Queue() self.topics = {} self.curstrs = QStringList() self.curlabel = None self.timer = QTimer(self) self.timer.timeout.connect(self.flush) self.timer.start(100) self.finished.connect(self.thread_finished) def abort(self): if self.isRunning() and hasattr(self, 'thread_id'): try: thread2._async_raise(self.thread_id, KeyboardInterrupt) except ValueError: pass def thread_finished(self): self.timer.stop() self.flush() self.commandFinished.emit(self.ret) def flush(self): if self.curlabel is not None: self.outputReceived.emit(self.curstrs.join(''), self.curlabel) self.curlabel = None if self.timer.isActive(): keys = self.topics.keys() for topic in keys: pos, item, unit, total = self.topics[topic] self.progressReceived.emit(topic, pos, item, unit, total) if pos is None: del self.topics[topic] else: # Close all progress bars for topic in self.topics: self.progressReceived.emit(topic, None, '', '', None) self.topics = {} @pyqtSlot(QString, QString) def output_handler(self, msg, label): if label == self.curlabel: self.curstrs.append(msg) else: if self.curlabel is not None: self.outputReceived.emit(self.curstrs.join(''), self.curlabel) self.curstrs = QStringList(msg) self.curlabel = label @pyqtSlot(QString, object, QString, QString, object) def progress_handler(self, topic, pos, item, unit, total): self.topics[topic] = (pos, item, unit, total) @pyqtSlot(DataWrapper) def interact_handler(self, wrapper): prompt, password, choices, default = wrapper.data prompt = hglib.tounicode(prompt) if choices: dlg = QMessageBox(QMessageBox.Question, _('TortoiseHg Prompt'), prompt, parent=self._parentWidget()) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) for index, choice in enumerate(choices): button = dlg.addButton(hglib.tounicode(choice), QMessageBox.ActionRole) button.response = index if index == default: dlg.setDefaultButton(button) dlg.exec_() button = dlg.clickedButton() if button is 0: self.responseq.put(None) else: self.responseq.put(button.response) else: mode = password and QLineEdit.Password \ or QLineEdit.Normal text, ok = qtlib.getTextInput(self._parentWidget(), _('TortoiseHg Prompt'), prompt.title(), mode=mode) if ok: text = hglib.fromunicode(text) else: text = None self.responseq.put(text) def _parentWidget(self): p = self.parent() while p and not p.isWidgetType(): p = p.parent() return p def run(self): ui = QtUi(responseq=self.responseq) ui.sig.writeSignal.connect(self.output_handler, Qt.QueuedConnection) ui.sig.progressSignal.connect(self.progress_handler, Qt.QueuedConnection) ui.sig.interactSignal.connect(self.interact_handler, Qt.QueuedConnection) try: # save thread id in order to terminate by KeyboardInterrupt self.thread_id = int(QThread.currentThreadId()) for k, v in ui.configitems('defaults'): ui.setconfig('defaults', k, '') # disable worker because it only works in main thread: # signal.signal(signal.SIGINT, signal.SIG_IGN) # ValueError: signal only works in main thread ui.setconfig('worker', 'numcpus', 1) self.ret = 255 self.ret = hglib.dispatch(ui, self.cmdline) or 0 except subrepo.SubrepoAbort, e: errormsg = str(e) label = 'ui.error' if e.subrepo: label += ' subrepo=%s' % urllib.quote(e.subrepo) ui.write_err(local._('abort: ') + errormsg + '\n', label=label) if e.hint: ui.write_err(local._('hint: ') + str(e.hint) + '\n', label=label) except util.Abort, e: ui.write_err(local._('abort: ') + str(e) + '\n') if e.hint: ui.write_err(local._('hint: ') + str(e.hint) + '\n') except error.RepoError, e: ui.write_err(str(e) + '\n') except urllib2.HTTPError, e: err = local._('HTTP Error: %d (%s)') % (e.code, e.msg) ui.write_err(err + '\n') except urllib2.URLError, e: err = local._('URLError: %s') % str(e.reason) try: import ssl # Python 2.6 or backport for 2.5 if isinstance(e.args[0], ssl.SSLError): parts = e.args[0].strerror.split(':') if len(parts) == 7: file, line, level, _errno, lib, func, reason = parts if func == 'SSL3_GET_SERVER_CERTIFICATE': err = local._('SSL: Server certificate verify failed') elif _errno == '00000000': err = local._('SSL: unknown error %s:%s') % (file, line) else: err = local._('SSL error: %s') % reason except ImportError: pass ui.write_err(err + '\n') except error.AmbiguousCommand, inst: ui.warn(local._("hg: command '%s' is ambiguous:\n %s\n") % (inst.args[0], " ".join(inst.args[1]))) except error.ParseError, inst: if len(inst.args) > 1: ui.warn(local._("hg: parse error at %s: %s\n") % (inst.args[1], inst.args[0])) else: ui.warn(local._("hg: parse error: %s\n") % inst.args[0]) except error.LockHeld, inst: if inst.errno == errno.ETIMEDOUT: reason = local._('timed out waiting for lock held by %s') % inst.locker else: reason = local._('lock held by %s') % inst.locker ui.warn(local._("abort: %s: %s\n") % (inst.desc or inst.filename, reason)) except error.LockUnavailable, inst: ui.warn(local._("abort: could not lock %s: %s\n") % (inst.desc or inst.filename, inst.strerror)) except error.CommandError, inst: if inst.args[0]: ui.warn(local._("hg %s: %s\n") % (inst.args[0], inst.args[1])) else: ui.warn(local._("hg: %s\n") % inst.args[1]) except error.OutOfBandError, inst: ui.warn(local._("abort: remote error:\n")) ui.warn(''.join(inst.args)) except error.RepoError, inst: ui.warn(local._("abort: %s!\n") % inst) except error.ResponseError, inst: ui.warn(local._("abort: %s") % inst.args[0]) if not isinstance(inst.args[1], basestring): ui.warn(" %r\n" % (inst.args[1],)) elif not inst.args[1]: ui.warn(local._(" empty string\n")) else: ui.warn("\n%r\n" % util.ellipsis(inst.args[1])) except error.RevlogError, inst: ui.warn(local._("abort: %s!\n") % inst) except error.UnknownCommand, inst: ui.warn(local._("hg: unknown command '%s'\n") % inst.args[0]) except error.InterventionRequired, inst: ui.warn("%s\n" % inst) self.ret = 1 except socket.error, inst: ui.warn(local._("abort: %s!\n") % str(inst)) except IOError, inst: if hasattr(inst, "code"): ui.warn(local._("abort: %s\n") % inst) elif hasattr(inst, "reason"): try: # usually it is in the form (errno, strerror) reason = inst.reason.args[1] except: # it might be anything, for example a string reason = inst.reason ui.warn(local._("abort: error: %s\n") % reason) elif hasattr(inst, "args") and inst.args[0] == errno.EPIPE: if ui.debugflag: ui.warn(local._("broken pipe\n")) elif getattr(inst, "strerror", None): if getattr(inst, "filename", None): ui.warn(local._("abort: %s: %s\n") % (inst.strerror, inst.filename)) else: ui.warn(local._("abort: %s\n") % inst.strerror) else: raise except OSError, inst: if getattr(inst, "filename", None): ui.warn(local._("abort: %s: %s\n") % (inst.strerror, inst.filename)) else: ui.warn(local._("abort: %s\n") % inst.strerror) except Exception, inst: ui.write_err(str(inst) + '\n') raise except KeyboardInterrupt: self.ret = -1 tortoisehg-2.10/tortoisehg/hgqt/shelve.py0000644000076400007640000004767412231647662017716 0ustar stevesteve# shelve.py - TortoiseHg shelve and patch tool # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import os import time from mercurial import commands, error, util from tortoisehg.util import hglib from tortoisehg.util.patchctx import patchctx from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, cmdui, chunks from PyQt4.QtCore import * from PyQt4.QtGui import * class ShelveDialog(QDialog): wdir = _('Working Directory') def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon('shelve')) self._repoagent = repoagent repo = repoagent.rawRepo() self.shelves = [] self.patches = [] layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(0) self.setLayout(layout) self.tbarhbox = hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(0) self.layout().addLayout(self.tbarhbox) self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Horizontal) self.splitter.setChildrenCollapsible(False) self.splitter.setObjectName('splitter') self.layout().addWidget(self.splitter, 1) aframe = QFrame(self.splitter) avbox = QVBoxLayout() avbox.setSpacing(2) avbox.setMargin(2) avbox.setContentsMargins(2, 2, 2, 2) aframe.setLayout(avbox) ahbox = QHBoxLayout() ahbox.setSpacing(2) ahbox.setMargin(2) ahbox.setContentsMargins(2, 2, 2, 2) avbox.addLayout(ahbox) self.comboa = QComboBox(self) self.comboa.setMinimumContentsLength(10) # allow to cut long content self.comboa.currentIndexChanged.connect(self.comboAChanged) self.clearShelfButtonA = QPushButton(_('Clear')) self.clearShelfButtonA.setToolTip(_('Clear the current shelf file')) self.clearShelfButtonA.clicked.connect(self.clearShelfA) self.delShelfButtonA = QPushButton(_('Delete')) self.delShelfButtonA.setToolTip(_('Delete the current shelf file')) self.delShelfButtonA.clicked.connect(self.deleteShelfA) ahbox.addWidget(self.comboa, 1) ahbox.addWidget(self.clearShelfButtonA) ahbox.addWidget(self.delShelfButtonA) self.browsea = chunks.ChunksWidget(self._repoagent, self, True) self.browsea.splitter.splitterMoved.connect(self.linkSplitters) self.browsea.linkActivated.connect(self.linkActivated) self.browsea.showMessage.connect(self.showMessage) avbox.addWidget(self.browsea) bframe = QFrame(self.splitter) bvbox = QVBoxLayout() bvbox.setSpacing(2) bvbox.setMargin(2) bvbox.setContentsMargins(2, 2, 2, 2) bframe.setLayout(bvbox) bhbox = QHBoxLayout() bhbox.setSpacing(2) bhbox.setMargin(2) bhbox.setContentsMargins(2, 2, 2, 2) bvbox.addLayout(bhbox) self.combob = QComboBox(self) self.combob.setMinimumContentsLength(10) # allow to cut long content self.combob.currentIndexChanged.connect(self.comboBChanged) self.clearShelfButtonB = QPushButton(_('Clear')) self.clearShelfButtonB.setToolTip(_('Clear the current shelf file')) self.clearShelfButtonB.clicked.connect(self.clearShelfB) self.delShelfButtonB = QPushButton(_('Delete')) self.delShelfButtonB.setToolTip(_('Delete the current shelf file')) self.delShelfButtonB.clicked.connect(self.deleteShelfB) bhbox.addWidget(self.combob, 1) bhbox.addWidget(self.clearShelfButtonB) bhbox.addWidget(self.delShelfButtonB) self.browseb = chunks.ChunksWidget(self._repoagent, self, True) self.browseb.splitter.splitterMoved.connect(self.linkSplitters) self.browseb.linkActivated.connect(self.linkActivated) self.browseb.showMessage.connect(self.showMessage) bvbox.addWidget(self.browseb) self.lefttbar = QToolBar(_('Left Toolbar'), objectName='lefttbar') self.lefttbar.setStyleSheet(qtlib.tbstylesheet) self.tbarhbox.addWidget(self.lefttbar) self.deletea = a = QAction(_('Delete selected chunks'), self) self.deletea.triggered.connect(self.browsea.deleteSelectedChunks) a.setIcon(qtlib.geticon('thg-shelve-delete-left')) self.lefttbar.addAction(self.deletea) self.allright = a = QAction(_('Move all files right'), self) self.allright.triggered.connect(self.moveFilesRight) a.setIcon(qtlib.geticon('thg-shelve-move-right-all')) self.lefttbar.addAction(self.allright) self.fileright = a = QAction(_('Move selected file right'), self) self.fileright.triggered.connect(self.moveFileRight) a.setIcon(qtlib.geticon('thg-shelve-move-right-file')) self.lefttbar.addAction(self.fileright) self.editfilea = a = QAction(_('Edit file'), self) a.setIcon(qtlib.geticon('edit-file')) self.lefttbar.addAction(self.editfilea) self.chunksright = a = QAction(_('Move selected chunks right'), self) self.chunksright.triggered.connect(self.moveChunksRight) a.setIcon(qtlib.geticon('thg-shelve-move-right-chunks')) self.lefttbar.addAction(self.chunksright) self.rbar = QToolBar(_('Refresh Toolbar'), objectName='rbar') self.rbar.setStyleSheet(qtlib.tbstylesheet) self.tbarhbox.addStretch(1) self.tbarhbox.addWidget(self.rbar) self.refreshAction = a = QAction(_('Refresh'), self) a.setIcon(qtlib.geticon('view-refresh')) a.setShortcuts(QKeySequence.Refresh) a.triggered.connect(self.refreshCombos) self.rbar.addAction(self.refreshAction) self.actionNew = a = QAction(_('New Shelf'), self) a.setIcon(qtlib.geticon('document-new')) a.triggered.connect(self.newShelfPressed) self.rbar.addAction(self.actionNew) self.righttbar = QToolBar(_('Right Toolbar'), objectName='righttbar') self.righttbar.setStyleSheet(qtlib.tbstylesheet) self.tbarhbox.addStretch(1) self.tbarhbox.addWidget(self.righttbar) self.chunksleft = a = QAction(_('Move selected chunks left'), self) self.chunksleft.triggered.connect(self.moveChunksLeft) a.setIcon(qtlib.geticon('thg-shelve-move-left-chunks')) self.righttbar.addAction(self.chunksleft) self.editfileb = a = QAction(_('Edit file'), self) a.setIcon(qtlib.geticon('edit-file')) self.righttbar.addAction(self.editfileb) self.fileleft = a = QAction(_('Move selected file left'), self) self.fileleft.triggered.connect(self.moveFileLeft) a.setIcon(qtlib.geticon('thg-shelve-move-left-file')) self.righttbar.addAction(self.fileleft) self.allleft = a = QAction(_('Move all files left'), self) self.allleft.triggered.connect(self.moveFilesLeft) a.setIcon(qtlib.geticon('thg-shelve-move-left-all')) self.righttbar.addAction(self.allleft) self.deleteb = a = QAction(_('Delete selected chunks'), self) self.deleteb.triggered.connect(self.browseb.deleteSelectedChunks) a.setIcon(qtlib.geticon('thg-shelve-delete-right')) self.righttbar.addAction(self.deleteb) self.editfilea.triggered.connect(self.browsea.editCurrentFile) self.editfileb.triggered.connect(self.browseb.editCurrentFile) self.browsea.chunksSelected.connect(self.chunksright.setEnabled) self.browsea.chunksSelected.connect(self.deletea.setEnabled) self.browsea.fileSelected.connect(self.fileright.setEnabled) self.browsea.fileSelected.connect(self.editfilea.setEnabled) self.browsea.fileModified.connect(self.refreshCombos) self.browsea.fileModelEmpty.connect(self.allright.setDisabled) self.browseb.chunksSelected.connect(self.chunksleft.setEnabled) self.browseb.chunksSelected.connect(self.deleteb.setEnabled) self.browseb.fileSelected.connect(self.fileleft.setEnabled) self.browseb.fileSelected.connect(self.editfileb.setEnabled) self.browseb.fileModified.connect(self.refreshCombos) self.browseb.fileModelEmpty.connect(self.allleft.setDisabled) self.statusbar = cmdui.ThgStatusBar(self) self.layout().addWidget(self.statusbar) self.showMessage(_('Backup copies of modified files can be found ' 'in .hg/Trashcan/')) self.refreshCombos() repoagent.repositoryChanged.connect(self.refreshCombos) self.setWindowTitle(_('TortoiseHg Shelve - %s') % repo.displayname) self.restoreSettings() @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def moveFileRight(self): if self.combob.currentIndex() == -1: self.newShelf(False) for file in self.browsea.getSelectedFiles(): chunks = self.browsea.getChunksForFile(file) if chunks and self.browseb.mergeChunks(file, chunks): self.browsea.removeFile(file) @pyqtSlot() def moveFileLeft(self): for file in self.browseb.getSelectedFiles(): chunks = self.browseb.getChunksForFile(file) if chunks and self.browsea.mergeChunks(file, chunks): self.browseb.removeFile(file) @pyqtSlot() def moveFilesRight(self): if self.combob.currentIndex() == -1: self.newShelf(False) for file in self.browsea.getFileList(): chunks = self.browsea.getChunksForFile(file) if chunks and self.browseb.mergeChunks(file, chunks): self.browsea.removeFile(file) @pyqtSlot() def moveFilesLeft(self): for file in self.browseb.getFileList(): chunks = self.browseb.getChunksForFile(file) if chunks and self.browsea.mergeChunks(file, chunks): self.browseb.removeFile(file) @pyqtSlot() def moveChunksRight(self): if self.combob.currentIndex() == -1: self.newShelf(False) file, chunks = self.browsea.getSelectedFileAndChunks() if self.browseb.mergeChunks(file, chunks): self.browsea.deleteSelectedChunks() @pyqtSlot() def moveChunksLeft(self): file, chunks = self.browseb.getSelectedFileAndChunks() if self.browsea.mergeChunks(file, chunks): self.browseb.deleteSelectedChunks() @pyqtSlot() def newShelfPressed(self): self.newShelf(True) def newShelf(self, interactive): shelve = time.strftime('%Y-%m-%d_%H-%M-%S') + \ '_parent_rev_%d' % self.repo['.'].rev() if interactive: name, ok = qtlib.getTextInput(self, _('TortoiseHg New Shelf Name'), _('Specify name of new shelf'), text=shelve) if not ok: return shelve = hglib.fromunicode(name) invalids = (':', '#', '/', '\\') bads = [c for c in shelve if c in invalids] if bads: qtlib.ErrorMsgBox(_('Bad filename'), _('A shelf name cannot contain %s') % ''.join(bads)) return badmsg = util.checkosfilename(shelve) if badmsg: qtlib.ErrorMsgBox(_('Bad filename'), hglib.tounicode(badmsg)) return try: fn = os.path.join('shelves', shelve) shelfpath = self.repo.join(fn) if os.path.exists(shelfpath): qtlib.ErrorMsgBox(_('File already exists'), _('A shelf file of that name already exists')) return self.repo.makeshelf(shelve) self.showMessage(_('New shelf created')) self.refreshCombos() if shelfpath in self.shelves: self.combob.setCurrentIndex(self.shelves.index(shelfpath)) except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) @pyqtSlot() def deleteShelfA(self): shelf = self.currentPatchA() ushelf = hglib.tounicode(os.path.basename(shelf)) if not qtlib.QuestionMsgBox(_('Are you sure?'), _('Delete shelf file %s?') % ushelf): return try: os.unlink(shelf) self.showMessage(_('Shelf deleted')) except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) self.refreshCombos() @pyqtSlot() def clearShelfA(self): if self.comboa.currentIndex() == 0: if not qtlib.QuestionMsgBox(_('Are you sure?'), _('Revert all working copy changes?')): return try: self.repo.ui.quiet = True commands.revert(self.repo.ui, self.repo, all=True) self.repo.ui.quiet = False except (EnvironmentError, error.Abort), e: self.showMessage(hglib.tounicode(str(e))) self.refreshCombos() return shelf = self.currentPatchA() ushelf = hglib.tounicode(os.path.basename(shelf)) if not qtlib.QuestionMsgBox(_('Are you sure?'), _('Clear contents of shelf file %s?') % ushelf): return try: f = open(shelf, "w") f.close() self.showMessage(_('Shelf cleared')) except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) self.refreshCombos() @pyqtSlot() def deleteShelfB(self): shelf = self.currentPatchB() ushelf = hglib.tounicode(os.path.basename(shelf)) if not qtlib.QuestionMsgBox(_('Are you sure?'), _('Delete shelf file %s?') % ushelf): return try: os.unlink(shelf) self.showMessage(_('Shelf deleted')) except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) self.refreshCombos() @pyqtSlot() def clearShelfB(self): shelf = self.currentPatchB() ushelf = hglib.tounicode(os.path.basename(shelf)) if not qtlib.QuestionMsgBox(_('Are you sure?'), _('Clear contents of shelf file %s?') % ushelf): return try: f = open(shelf, "w") f.close() self.showMessage(_('Shelf cleared')) except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) self.refreshCombos() def currentPatchA(self): idx = self.comboa.currentIndex() if idx == -1: return None if idx == 0: return self.wdir idx -= 1 if idx < len(self.shelves): return self.shelves[idx] idx -= len(self.shelves) if idx < len(self.patches): return self.patches[idx] return None def currentPatchB(self): idx = self.combob.currentIndex() if idx == -1: return None if idx < len(self.shelves): return self.shelves[idx] idx -= len(self.shelves) if idx < len(self.patches): return self.patches[idx] return None @pyqtSlot() def refreshCombos(self): shelvea, shelveb = self.currentPatchA(), self.currentPatchB() # Note that thgshelves returns the shelve list ordered from newest to # oldest shelves = self.repo.thgshelves() disp = [_('Shelf: %s') % hglib.tounicode(s) for s in shelves] patches = self.repo.thgmqunappliedpatches disp += [_('Patch: %s') % hglib.tounicode(p) for p in patches] # store fully qualified paths self.shelves = [os.path.join(self.repo.shelfdir, s) for s in shelves] self.patches = [self.repo.mq.join(p) for p in patches] self.comboRefreshInProgress = True self.comboa.clear() self.combob.clear() self.comboa.addItems([self.wdir] + disp) self.combob.addItems(disp) # attempt to restore selection if shelvea == self.wdir: idxa = 0 elif shelvea in self.shelves: idxa = self.shelves.index(shelvea) + 1 elif shelvea in self.patches: idxa = len(self.shelves) + self.patches.index(shelvea) + 1 else: idxa = 0 self.comboa.setCurrentIndex(idxa) if shelveb in self.shelves: idxb = self.shelves.index(shelveb) elif shelveb in self.patches: idxb = len(self.shelves) + self.patches.index(shelveb) else: idxb = 0 self.combob.setCurrentIndex(idxb) self.comboRefreshInProgress = False self.comboAChanged(idxa) self.comboBChanged(idxb) if not patches and not shelves: self.delShelfButtonB.setEnabled(False) self.clearShelfButtonB.setEnabled(False) self.browseb.setContext(patchctx('', self.repo, None)) @pyqtSlot(int) def comboAChanged(self, index): if self.comboRefreshInProgress: return assert index >= 0 # side A should always have "Working Directory" if index == 0: rev = None self.delShelfButtonA.setEnabled(False) self.clearShelfButtonA.setEnabled(True) else: rev = self.currentPatchA() self.delShelfButtonA.setEnabled(index <= len(self.shelves)) self.clearShelfButtonA.setEnabled(index <= len(self.shelves)) self.browsea.setContext(self.repo.changectx(rev)) self._deselectSamePatch(self.combob) @pyqtSlot(int) def comboBChanged(self, index): if self.comboRefreshInProgress: return rev = self.currentPatchB() self.delShelfButtonB.setEnabled(0 <= index < len(self.shelves)) self.clearShelfButtonB.setEnabled(0 <= index < len(self.shelves)) self.browseb.setContext(self.repo.changectx(rev)) self._deselectSamePatch(self.comboa) def _deselectSamePatch(self, combo): # if the same patch or shelve is selected by both sides, "move" action # will corrupt patch content. if self.currentPatchA() != self.currentPatchB(): return if combo.count() > 1: combo.setCurrentIndex((combo.currentIndex() + 1) % combo.count()) else: combo.setCurrentIndex(-1) @pyqtSlot(int, int) def linkSplitters(self, pos, index): if self.browsea.splitter.sizes()[0] != pos: self.browsea.splitter.moveSplitter(pos, index) if self.browseb.splitter.sizes()[0] != pos: self.browseb.splitter.moveSplitter(pos, index) @pyqtSlot(QString) def linkActivated(self, linktext): pass @pyqtSlot(QString) def showMessage(self, message): self.statusbar.showMessage(message) def storeSettings(self): s = QSettings() wb = "shelve/" s.setValue(wb + 'geometry', self.saveGeometry()) s.setValue(wb + 'panesplitter', self.splitter.saveState()) s.setValue(wb + 'filesplitter', self.browsea.splitter.saveState()) self.browsea.saveSettings(s, wb + 'fileviewa') self.browseb.saveSettings(s, wb + 'fileviewb') def restoreSettings(self): s = QSettings() wb = "shelve/" self.restoreGeometry(s.value(wb + 'geometry').toByteArray()) self.splitter.restoreState(s.value(wb + 'panesplitter').toByteArray()) self.browsea.splitter.restoreState( s.value(wb + 'filesplitter').toByteArray()) self.browseb.splitter.restoreState( s.value(wb + 'filesplitter').toByteArray()) self.browsea.loadSettings(s, wb + 'fileviewa') self.browseb.loadSettings(s, wb + 'fileviewb') def reject(self): self.storeSettings() super(ShelveDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/postreview.ui0000644000076400007640000002674312110205646020602 0ustar stevesteve PostReviewDialog 0 0 660 459 Review Board 16777215 110 0 Post Review Repository ID: repo_id_combo 0 0 false QComboBox::InsertAtTop Summary: summary_edit 0 0 true QComboBox::InsertAtTop Update Review Review ID: review_id_combo 0 0 false QComboBox::InsertAtTop Update the fields of this existing request Options Create diff with all outgoing changes Create diff with all changes on this branch Publish request immediately true Changesets 0 false false false false &Settings false Qt::Horizontal 40 20 200 0 true 0 0 -1 Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true Qt::Horizontal false QProgressBar::TopToBottom %p% Connecting to Review Board... Qt::Horizontal 0 20 false Post &Review false true &Close true changesets_view post_review_button settings_button post_review_button clicked() PostReviewDialog accept() 20 20 20 20 settings_button clicked() PostReviewDialog onSettingsButtonClicked() 20 20 20 20 close_button clicked() PostReviewDialog close() 20 20 20 20 outgoing_changes_check toggled(bool) PostReviewDialog outgoingChangesCheckToggle() 20 20 20 20 branch_check toggled(bool) PostReviewDialog branchCheckToggle() 20 20 20 20 tab_widget currentChanged(int) PostReviewDialog tabChanged() 20 20 20 20 tortoisehg-2.10/tortoisehg/hgqt/status.py0000644000076400007640000012266212235634453017740 0ustar stevesteve# status.py - working copy browser # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import hg, util, error, context, merge, scmutil from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, wctxactions, visdiff, cmdui, fileview, thgrepo from PyQt4.QtCore import * from PyQt4.QtGui import * # This widget can be used as the basis of the commit tool or any other # working copy browser. # Technical Debt # We need a real icon set for file status types # Thread rowSelected, connect to an external progress bar # Chunk selection, tri-state checkboxes for commit # Maybe, Maybe Not # Investigate folding/nesting of files COL_PATH = 0 COL_STATUS = 1 COL_MERGE_STATE = 2 COL_PATH_DISPLAY = 3 COL_EXTENSION = 4 COL_SIZE = 5 _colors = {} class StatusWidget(QWidget): '''Working copy status widget SIGNALS: progress() - for progress bar showMessage(unicode) - for status bar titleTextChanged(QString) - for window title ''' progress = pyqtSignal(QString, object, QString, QString, object) titleTextChanged = pyqtSignal(QString) linkActivated = pyqtSignal(QString) showMessage = pyqtSignal(unicode) fileDisplayed = pyqtSignal(QString, QString) grepRequested = pyqtSignal(unicode, dict) runCustomCommandRequested = pyqtSignal(str, list) def __init__(self, repoagent, pats, opts, parent=None, checkable=True, defcheck='commit'): QWidget.__init__(self, parent) self.opts = dict(modified=True, added=True, removed=True, deleted=True, unknown=True, clean=False, ignored=False, subrepo=True) self.opts.update(opts) self._repoagent = repoagent self.pats = pats self.checkable = checkable self.defcheck = defcheck self.pctx = None self.savechecks = True self.refthread = None self.refreshWctxLater = QTimer(self, interval=10, singleShot=True) self.refreshWctxLater.timeout.connect(self.refreshWctx) self.partials = {} # determine the user configured status colors # (in the future, we could support full rich-text tags) labels = [(stat, val.uilabel) for stat, val in statusTypes.items()] labels.extend([('r', 'resolve.resolved'), ('u', 'resolve.unresolved')]) for stat, label in labels: effect = qtlib.geteffect(label) for e in effect.split(';'): if e.startswith('color:'): _colors[stat] = QColor(e[7:]) break SP = QSizePolicy split = QSplitter(Qt.Horizontal) split.setChildrenCollapsible(False) layout = QVBoxLayout() layout.setMargin(0) layout.addWidget(split) self.setLayout(layout) vbox = QVBoxLayout() vbox.setMargin(0) frame = QFrame(split) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(0) frame.setSizePolicy(sp) frame.setLayout(vbox) hbox = QHBoxLayout() hbox.setMargin(4) hbox.setContentsMargins(0, 0, 0, 0) self.refreshBtn = tb = QToolButton() tb.setToolTip(_('Refresh file list')) tb.setIcon(qtlib.geticon('view-refresh')) tb.clicked.connect(self.refreshWctx) le = QLineEdit() if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### filter text ###')) else: lbl = QLabel(_('Filter:')) hbox.addWidget(lbl) st = '' for s in statusTypes: val = statusTypes[s] if self.opts[val.name]: st = st + s self.statusfilter = StatusFilterButton( statustext=st, types=StatusType.preferredOrder) if self.checkable: self.checkAllTT = _('Check all files') self.checkNoneTT = _('Uncheck all files') self.checkAllNoneBtn = QCheckBox() self.checkAllNoneBtn.setToolTip(self.checkAllTT) self.checkAllNoneBtn.clicked.connect(self.checkAllNone) self.filelistToolbar = QToolBar(_('Status File List Toolbar')) self.filelistToolbar.setIconSize(QSize(16,16)) self.filelistToolbar.setStyleSheet(qtlib.tbstylesheet) hbox.addWidget(self.filelistToolbar) if self.checkable: self.filelistToolbar.addWidget(qtlib.Spacer(3, 2)) self.filelistToolbar.addWidget(self.checkAllNoneBtn) self.filelistToolbar.addSeparator() self.filelistToolbar.addWidget(le) self.filelistToolbar.addSeparator() self.filelistToolbar.addWidget(self.statusfilter) self.filelistToolbar.addSeparator() self.filelistToolbar.addWidget(self.refreshBtn) self.actions = wctxactions.WctxActions(self._repoagent, self, checkable) self.actions.refreshNeeded.connect(self.refreshWctx) self.actions.runCustomCommandRequested.connect( self.runCustomCommandRequested) tv = WctxFileTree(self.repo, checkable=checkable) vbox.addLayout(hbox) vbox.addWidget(tv) split.addWidget(frame) self.clearPatternBtn = QPushButton(_('Remove filter, show root')) vbox.addWidget(self.clearPatternBtn) self.clearPatternBtn.clicked.connect(self.clearPattern) self.clearPatternBtn.setVisible(bool(self.pats)) tv.setItemsExpandable(False) tv.setRootIsDecorated(False) tv.sortByColumn(COL_STATUS, Qt.AscendingOrder) tv.clicked.connect(self.onRowClicked) tv.doubleClicked.connect(self.onRowDoubleClicked) tv.menuRequest.connect(self.onMenuRequest) le.textEdited.connect(self.setFilter) self.statusfilter.statusChanged.connect(self.setStatusFilter) self.tv = tv self.le = le # Diff panel side of splitter vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setContentsMargins(0, 0, 0, 0) docf = QFrame(split) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(0) docf.setSizePolicy(sp) docf.setLayout(vbox) self.docf = docf self.fileview = fileview.HgFileView(self._repoagent, self) self.fileview.showMessage.connect(self.showMessage) self.fileview.linkActivated.connect(self.linkActivated) self.fileview.fileDisplayed.connect(self.fileDisplayed) self.fileview.shelveToolExited.connect(self.refreshWctx) self.fileview.newChunkList.connect(self.updatePartials) self.fileview.chunkSelectionChanged.connect(self.chunkSelectionChanged) self.fileview.grepRequested.connect(self.grepRequested) self.fileview.setContext(self.repo[None]) self.fileview.setMinimumSize(QSize(16, 16)) vbox.addWidget(self.fileview, 1) self.split = split self.diffvbox = vbox @property def repo(self): return self._repoagent.rawRepo() def __get_defcheck(self): if self._defcheck is None: return 'MAR!S' return self._defcheck def __set_defcheck(self, newdefcheck): if newdefcheck.lower() == 'amend': newdefcheck = 'MAS' elif newdefcheck.lower() in ('commit', 'qnew', 'qrefresh'): newdefcheck = 'MAR!S' self._defcheck = newdefcheck defcheck = property(__get_defcheck, __set_defcheck) @pyqtSlot() def checkAllNone(self): state = self.checkAllNoneBtn.checkState() if state == Qt.Checked: self.checkAll() self.checkAllNoneBtn.setToolTip(self.checkNoneTT) else: if state == Qt.Unchecked: self.checkNone() self.checkAllNoneBtn.setToolTip(self.checkAllTT) if state != Qt.PartiallyChecked: self.checkAllNoneBtn.setTristate(False) def getTitle(self): if self.pats: return _('%s - status (selection filtered)') % self.repo.displayname else: return _('%s - status') % self.repo.displayname def loadSettings(self, qs, prefix): self.fileview.loadSettings(qs, prefix+'/fileview') self.split.restoreState(qs.value(prefix+'/state').toByteArray()) def saveSettings(self, qs, prefix): self.fileview.saveSettings(qs, prefix+'/fileview') qs.setValue(prefix+'/state', self.split.saveState()) @pyqtSlot(QString, object) def updatePartials(self, wfile, changes): # remove files from the partials dictionary if they are not partial # selections, in order to simplify refresh. dels = [] for file, oldchanges in self.partials.iteritems(): assert file in self.tv.model().checked if oldchanges.excludecount == 0: self.tv.model().checked[file] = True dels.append(file) elif oldchanges.excludecount == len(oldchanges.hunks): self.tv.model().checked[file] = False dels.append(file) for file in dels: del self.partials[file] wfile = hglib.fromunicode(wfile) if changes is None: if wfile in self.partials: del self.partials[wfile] self.chunkSelectionChanged() return if wfile in self.partials: # merge selection state from old hunk list to new hunk list oldhunks = self.partials[wfile].hunks oldstates = dict([(c.fromline, c.excluded) for c in oldhunks]) for chunk in changes.hunks: if chunk.fromline in oldstates: self.fileview.updateChunk(chunk, oldstates[chunk.fromline]) else: # the file was not in the partials dictionary, so it is either # checked (all changes enabled) or unchecked (all changes # excluded). if wfile not in self.getChecked(): for chunk in changes.hunks: self.fileview.updateChunk(chunk, True) self.chunkSelectionChanged() self.partials[wfile] = changes @pyqtSlot() def chunkSelectionChanged(self): 'checkbox state has changed via chunk selection' # inform filelist view that the file selection state may have changed model = self.tv.model() if model: model.layoutChanged.emit() model.checkCountChanged.emit() @pyqtSlot(QPoint, object) def onMenuRequest(self, point, selected): menu = self.actions.makeMenu(selected) menu.exec_(point) def setPatchContext(self, pctx): if pctx != self.pctx: # clear out the current checked state on next refreshWctx() self.savechecks = False self.pctx = pctx @pyqtSlot() def refreshWctx(self): if self.refthread: self.refreshWctxLater.start() return self.refreshWctxLater.stop() self.fileview.clearDisplay() # store selected paths or current path model = self.tv.model() if model and model.rowCount(QModelIndex()): smodel = self.tv.selectionModel() curidx = smodel.currentIndex() if curidx.isValid(): curpath = model.getRow(curidx)[COL_PATH] else: curpath = None spaths = [model.getRow(i)[COL_PATH] for i in smodel.selectedRows()] self.reselection = spaths, curpath else: self.reselection = None if self.checkable: self.checkAllNoneBtn.setEnabled(False) self.refreshBtn.setEnabled(False) self.progress.emit(*cmdui.startProgress(_('Refresh'), _('status'))) self.refthread = StatusThread(self.repo, self.pctx, self.pats, self.opts) self.refthread.finished.connect(self.reloadComplete) self.refthread.showMessage.connect(self.reloadFailed) self.refthread.start() @pyqtSlot() def reloadComplete(self): self.refthread.wait() if self.checkable: self.checkAllNoneBtn.setEnabled(True) self.refreshBtn.setEnabled(True) self.progress.emit(*cmdui.stopProgress(_('Refresh'))) if self.refthread.wctx is not None: self.updateModel(self.refthread.wctx, self.refthread.patchecked) self.refthread = None if len(self.repo.parents()) > 1: # nuke partial selections if wctx has a merge in-progress self.partials = {} match = self.le.text() if match: self.setFilter(match) # better to handle error in reloadComplete in place of separate signal? @pyqtSlot(QString) def reloadFailed(self, msg): qtlib.ErrorMsgBox(_('Failed to refresh'), msg, parent=self) def isRefreshingWctx(self): return bool(self.refthread) def canExit(self): return not self.isRefreshingWctx() def updateModel(self, wctx, patchecked): self.tv.setSortingEnabled(False) if self.tv.model(): checked = self.tv.model().getChecked() else: checked = patchecked if self.pats and not checked: qtlib.WarningMsgBox(_('No appropriate files'), _('No files found for this operation'), parent=self) ms = merge.mergestate(self.repo) tm = WctxModel(wctx, ms, self.pctx, self.savechecks, self.opts, checked, self, checkable=self.checkable, defcheck=self.defcheck) if self.checkable: tm.checkToggled.connect(self.checkToggled) tm.checkCountChanged.connect(self.updateCheckCount) self.savechecks = True oldtm = self.tv.model() self.tv.setModel(tm) if oldtm: oldtm.deleteLater() self.tv.setSortingEnabled(True) self.tv.setColumnHidden(COL_PATH, bool(wctx.p2()) or not self.checkable) self.tv.setColumnHidden(COL_MERGE_STATE, not tm.anyMerge()) if self.checkable: self.updateCheckCount() # remove non-existent file from partials table because model changed for file in self.partials.keys(): if file not in tm.checked: del self.partials[file] for col in (COL_PATH, COL_STATUS, COL_MERGE_STATE): w = self.tv.sizeHintForColumn(col) self.tv.setColumnWidth(col, w) for col in (COL_PATH_DISPLAY, COL_EXTENSION, COL_SIZE): self.tv.resizeColumnToContents(col) # reset selection, or select first row curidx = tm.index(0, 0) selmodel = self.tv.selectionModel() flags = QItemSelectionModel.Select | QItemSelectionModel.Rows if self.reselection: selected, current = self.reselection for i, row in enumerate(tm.getAllRows()): if row[COL_PATH] in selected: selmodel.select(tm.index(i, 0), flags) if row[COL_PATH] == current: curidx = tm.index(i, 0) else: selmodel.select(curidx, flags) selmodel.currentChanged.connect(self.onCurrentChange) selmodel.selectionChanged.connect(self.onSelectionChange) if curidx and curidx.isValid(): selmodel.setCurrentIndex(curidx, QItemSelectionModel.Current) self.onSelectionChange(None, None) # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QModelIndex) def onRowClicked(self, index): 'tree view emitted a clicked signal, index guarunteed valid' if index.column() == COL_PATH: self.tv.model().toggleRows([index]) # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QModelIndex) def onRowDoubleClicked(self, index): 'tree view emitted a doubleClicked signal, index guarunteed valid' path, status, mst, u, ext, sz = self.tv.model().getRow(index) if status in 'MAR!': self.actions.allactions[0].trigger() elif status == 'S': self.linkActivated.emit( u'repo:' + hglib.tounicode(self.repo.wjoin(path))) elif status in 'C?': qtlib.editfiles(self.repo, [path]) @pyqtSlot(str) def setStatusFilter(self, status): status = str(status) for s in statusTypes: val = statusTypes[s] self.opts[val.name] = s in status self.refreshWctx() @pyqtSlot(QString) def setFilter(self, match): model = self.tv.model() if model: model.setFilter(match) self.tv.enablefilterpalette(bool(match)) @pyqtSlot() def clearPattern(self): self.pats = [] self.refreshWctx() self.clearPatternBtn.setVisible(False) self.titleTextChanged.emit(self.getTitle()) @pyqtSlot() def updateCheckCount(self): 'user has toggled one or more checkboxes, update counts and checkall' model = self.tv.model() if model: model.checkCount = len(self.getChecked()) if model.checkCount == 0: state = Qt.Unchecked elif model.checkCount == len(model.rows): state = Qt.Checked else: state = Qt.PartiallyChecked self.checkAllNoneBtn.setTristate(state == Qt.PartiallyChecked) self.checkAllNoneBtn.setCheckState(state) @pyqtSlot(QString, bool) def checkToggled(self, wfile, checked): 'user has toggled a checkbox, update partial chunk selection status' wfile = hglib.fromunicode(wfile) if wfile in self.partials: if wfile == self.fileview._filename: for chunk in self.partials[wfile].hunks: self.fileview.updateChunk(chunk, not checked) else: del self.partials[wfile] def checkAll(self): model = self.tv.model() if model: model.checkAll(True) def checkNone(self): model = self.tv.model() if model: model.checkAll(False) def getChecked(self, types=None): model = self.tv.model() if model: checked = model.getChecked() if types is None: files = [] for f, v in checked.iteritems(): if f in self.partials: changes = self.partials[f] if changes.excludecount < len(changes.hunks): files.append(f) elif v: files.append(f) return files else: files = [] for row in model.getAllRows(): path, status, mst, upath, ext, sz = row if status in types: if path in self.partials: changes = self.partials[path] if changes.excludecount < len(changes.hunks): files.append(path) elif checked[path]: files.append(path) return files else: return [] # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QItemSelection, QItemSelection) def onSelectionChange(self, selected, deselected): selrows = [] for index in self.tv.selectedRows(): path, status, mst, u, ext, sz = self.tv.model().getRow(index) selrows.append((set(status+mst.lower()), path)) self.actions.updateActionSensitivity(selrows) # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QModelIndex, QModelIndex) def onCurrentChange(self, index, old): 'Connected to treeview "currentChanged" signal' row = index.model().getRow(index) if row is None: return path, status, mst, upath, ext, sz = row wfile = util.pconvert(path) pctx = self.pctx and self.pctx.p1() or None self.fileview.setContext(self.repo[None], pctx) self.fileview.displayFile(wfile, status) class StatusThread(QThread): '''Background thread for generating a workingctx''' showMessage = pyqtSignal(QString) def __init__(self, repo, pctx, pats, opts, parent=None): super(StatusThread, self).__init__() self.repo = hg.repository(repo.ui, repo.root) self.pctx = pctx self.pats = pats self.opts = opts self.wctx = None self.patchecked = {} def run(self): self.repo.dirstate.invalidate() extract = lambda x, y: dict(zip(x, map(y.get, x))) stopts = extract(('unknown', 'ignored', 'clean'), self.opts) patchecked = {} try: if self.pats: if self.opts.get('checkall'): # quickop sets this flag to pre-check even !?IC files precheckfn = lambda x: True else: # status and commit only pre-check MAR files precheckfn = lambda x: x < 4 m = scmutil.match(self.repo[None], self.pats) self.repo.bfstatus = True self.repo.lfstatus = True status = self.repo.status(match=m, **stopts) self.repo.bfstatus = False self.repo.lfstatus = False # Record all matched files as initially checked for i, stat in enumerate(StatusType.preferredOrder): if stat == 'S': continue val = statusTypes[stat] if self.opts[val.name]: d = dict([(fn, precheckfn(i)) for fn in status[i]]) patchecked.update(d) wctx = context.workingctx(self.repo, changes=status) self.patchecked = patchecked elif self.pctx: self.repo.bfstatus = True self.repo.lfstatus = True status = self.repo.status(node1=self.pctx.p1().node(), **stopts) self.repo.bfstatus = False self.repo.lfstatus = False wctx = context.workingctx(self.repo, changes=status) else: wctx = self.repo[None] self.repo.bfstatus = True self.repo.lfstatus = True wctx.status(**stopts) self.repo.bfstatus = False self.repo.lfstatus = False self.wctx = wctx wctx.dirtySubrepos = [] for s in wctx.substate: if wctx.sub(s).dirty(): wctx.dirtySubrepos.append(s) except EnvironmentError, e: self.showMessage.emit(hglib.tounicode(str(e))) except (error.LookupError, error.RepoError, error.ConfigError), e: self.showMessage.emit(hglib.tounicode(str(e))) except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) self.showMessage.emit(err) class WctxFileTree(QTreeView): menuRequest = pyqtSignal(QPoint, object) def __init__(self, repo, parent=None, checkable=True): QTreeView.__init__(self, parent) self.repo = repo self.setSelectionMode(QTreeView.ExtendedSelection) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.menuRequested) self.setTextElideMode(Qt.ElideLeft) self._paletteswitcher = qtlib.PaletteSwitcher(self) def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible): # don't update horizontal position by selection change orighoriz = self.horizontalScrollBar().value() super(WctxFileTree, self).scrollTo(index, hint) self.horizontalScrollBar().setValue(orighoriz) def keyPressEvent(self, event): if event.key() == 32: self.model().toggleRows(self.selectedRows()) return super(WctxFileTree, self).keyPressEvent(event) def dragObject(self): urls = [] for index in self.selectedRows(): path = self.model().getRow(index)[COL_PATH] urls.append(QUrl.fromLocalFile(self.repo.wjoin(path))) if urls: drag = QDrag(self) data = QMimeData() data.setUrls(urls) drag.setMimeData(data) drag.start(Qt.CopyAction) def mousePressEvent(self, event): self.pressPos = event.pos() self.pressTime = QTime.currentTime() return QTreeView.mousePressEvent(self, event) def mouseMoveEvent(self, event): d = event.pos() - self.pressPos if d.manhattanLength() < QApplication.startDragDistance(): return QTreeView.mouseMoveEvent(self, event) elapsed = self.pressTime.msecsTo(QTime.currentTime()) if elapsed < QApplication.startDragTime(): return QTreeView.mouseMoveEvent(self, event) self.dragObject() return QTreeView.mouseMoveEvent(self, event) def menuRequested(self, point): selrows = [] for index in self.selectedRows(): path, status, mst, u, ext, sz = self.model().getRow(index) selrows.append((set(status+mst.lower()), path)) if selrows: self.menuRequest.emit(self.viewport().mapToGlobal(point), selrows) def selectedRows(self): if self.selectionModel(): return self.selectionModel().selectedRows() # Invalid selectionModel found return [] def enablefilterpalette(self, enable): self._paletteswitcher.enablefilterpalette(enable) class WctxModel(QAbstractTableModel): checkCountChanged = pyqtSignal() checkToggled = pyqtSignal(QString, bool) def __init__(self, wctx, ms, pctx, savechecks, opts, checked, parent, checkable=True, defcheck='MAR!S'): QAbstractTableModel.__init__(self, parent) self.partials = parent.partials self.checkCount = 0 rows = [] nchecked = {} excludes = [f.strip() for f in opts.get('ciexclude', '').split(',')] def mkrow(fname, st): ext, sizek = '', '' try: mst = fname in ms and ms[fname].upper() or "" name, ext = os.path.splitext(fname) sizebytes = wctx[fname].size() sizek = (sizebytes + 1023) // 1024 except EnvironmentError: pass return [fname, st, mst, hglib.tounicode(fname), ext[1:], sizek] if not savechecks: checked = {} if pctx: # Currently, having a patch context means it's a qrefresh, so only # auto-check files in pctx.files() pctxfiles = pctx.files() pctxmatch = lambda f: f in pctxfiles else: pctxmatch = lambda f: True if opts['modified']: for m in wctx.modified(): nchecked[m] = checked.get(m, 'M' in defcheck and m not in excludes and pctxmatch(m)) rows.append(mkrow(m, 'M')) if opts['added']: for a in wctx.added(): nchecked[a] = checked.get(a, 'A' in defcheck and a not in excludes and pctxmatch(a)) rows.append(mkrow(a, 'A')) if opts['removed']: for r in wctx.removed(): nchecked[r] = checked.get(r, 'R' in defcheck and r not in excludes and pctxmatch(r)) rows.append(mkrow(r, 'R')) if opts['deleted']: for d in wctx.deleted(): nchecked[d] = checked.get(d, 'D' in defcheck and d not in excludes and pctxmatch(d)) rows.append(mkrow(d, '!')) if opts['unknown']: for u in wctx.unknown() or []: nchecked[u] = checked.get(u, '?' in defcheck) rows.append(mkrow(u, '?')) if opts['ignored']: for i in wctx.ignored() or []: nchecked[i] = checked.get(i, 'I' in defcheck) rows.append(mkrow(i, 'I')) if opts['clean']: for c in wctx.clean() or []: nchecked[c] = checked.get(c, 'C' in defcheck) rows.append(mkrow(c, 'C')) if opts['subrepo']: for s in wctx.dirtySubrepos: nchecked[s] = checked.get(s, 'S' in defcheck) rows.append(mkrow(s, 'S')) # include clean unresolved files for f in ms: if ms[f] == 'u' and f not in nchecked: nchecked[f] = checked.get(f, True) rows.append(mkrow(f, 'C')) self.headers = ('*', _('Stat'), _('M'), _('Filename'), _('Type'), _('Size (KB)')) self.checked = nchecked self.unfiltered = rows self.rows = rows self.checkable = checkable def rowCount(self, parent): if parent.isValid(): return 0 # no child return len(self.rows) def check(self, files, state): for f in files: assert f in checked self.checked[f] = state self.checkToggled.emit(f, state) self.layoutChanged.emit() self.checkCountChanged.emit() def checkAll(self, state): for data in self.rows: self.checked[data[0]] = state self.checkToggled.emit(data[3], state) self.layoutChanged.emit() self.checkCountChanged.emit() def columnCount(self, parent): if parent.isValid(): return 0 # no child return len(self.headers) def data(self, index, role): if not index.isValid(): return QVariant() path, status, mst, upath, ext, sz = self.rows[index.row()] if index.column() == COL_PATH: if role == Qt.CheckStateRole and self.checkable: if path in self.partials: changes = self.partials[path] if changes.excludecount == 0: return Qt.Checked elif changes.excludecount == len(changes.hunks): return Qt.Unchecked else: return Qt.PartiallyChecked if self.checked[path]: return Qt.Checked else: return Qt.Unchecked elif role == Qt.DisplayRole: return QVariant("") elif role == Qt.ToolTipRole: return QVariant(_('Checked count: %d') % self.checkCount) elif role == Qt.DisplayRole: return QVariant(self.rows[index.row()][index.column()]) elif role == Qt.TextColorRole: if mst: return _colors.get(mst.lower(), QColor('black')) else: return _colors.get(status, QColor('black')) elif role == Qt.ToolTipRole: return QVariant(statusMessage(status, mst, upath)) ''' elif role == Qt.DecorationRole and index.column() == COL_STATUS: if status in statusTypes: ico = QIcon() ico.addPixmap(QPixmap('icons/' + statusTypes[status].icon)) return QVariant(ico) ''' return QVariant() def headerData(self, col, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() else: return QVariant(self.headers[col]) def flags(self, index): flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled if index.column() == COL_PATH and self.checkable: flags |= Qt.ItemIsUserCheckable return flags # Custom methods def anyMerge(self): for r in self.rows: if r[COL_MERGE_STATE]: return True return False def getRow(self, index): assert index.isValid() return self.rows[index.row()] def getAllRows(self): for row in self.rows: yield row def toggleRows(self, indexes): 'Connected to "activated" signal, emitted by dbl-click or enter' if QApplication.keyboardModifiers() & Qt.ControlModifier: # ignore Ctrl-Enter events, the user does not want a row # toggled just as they are committing. return self.layoutAboutToBeChanged.emit() for index in indexes: assert index.isValid() fname = self.rows[index.row()][COL_PATH] uname = self.rows[index.row()][COL_PATH_DISPLAY] if fname in self.partials: checked = 0 changes = self.partials[fname] if changes.excludecount < len(changes.hunks): checked = 1 else: checked = self.checked[fname] self.checked[fname] = not checked self.checkToggled.emit(uname, self.checked[fname]) self.layoutChanged.emit() self.checkCountChanged.emit() def sort(self, col, order): self.layoutAboutToBeChanged.emit() def getStatusRank(value): """Helper function used to sort items according to their hg status Statuses are ranked in the following order: 'S','M','A','R','!','?','C','I','' """ sortList = ['S','M','A','R','!','?','C','I',''] try: rank = sortList.index(value) except (IndexError, ValueError): rank = len(sortList) # Set the lowest rank by default return rank def getMergeStatusRank(value): """Helper function used to sort according to item merge status Merge statuses are ranked in the following order: 'S','U','R','' """ sortList = ['S','U','R',''] try: rank = sortList.index(value) except (IndexError, ValueError): rank = len(sortList) # Set the lowest rank by default return rank # We want to sort the list by one of the columns (checked state, # mercurial status, file path, file extension, etc) # However, for files which have the same status or extension, etc, # we want them to be sorted alphabetically (without taking into account # the case) # Since Python 2.3 the sort function is guaranteed to be stable. # Thus we can perform the sort in two passes: # 1.- Perform a secondary sort by path # 2.- Perform a primary sort by the actual column that we are sorting on # Secondary sort: self.rows.sort(key=lambda x: x[COL_PATH].lower()) if col == COL_PATH_DISPLAY: # Already sorted! pass else: if order == Qt.DescendingOrder: # We want the secondary sort to be by _ascending_ path, # even when the primary sort is in descending order self.rows.reverse() # Now we can perform the primary sort if col == COL_PATH: c = self.checked self.rows.sort(key=lambda x: c[x[col]]) elif col == COL_STATUS: self.rows.sort(key=lambda x: getStatusRank(x[col])) elif col == COL_MERGE_STATE: self.rows.sort(key=lambda x: getMergeStatusRank(x[col])) else: self.rows.sort(key=lambda x: x[col]) if order == Qt.DescendingOrder: self.rows.reverse() self.layoutChanged.emit() self.reset() def setFilter(self, match): 'simple match in filename filter' self.layoutAboutToBeChanged.emit() self.rows = [r for r in self.unfiltered if unicode(match) in r[COL_PATH_DISPLAY]] self.layoutChanged.emit() self.reset() def getChecked(self): assert len(self.checked) == len(self.unfiltered) return self.checked.copy() def statusMessage(status, mst, upath): tip = '' if status in statusTypes: upath = "%s " % upath tip = statusTypes[status].desc % upath if mst == 'R': tip += _(', resolved merge') elif mst == 'U': tip += _(', unresolved merge') return tip class StatusType(object): preferredOrder = 'MAR!?ICS' def __init__(self, name, icon, desc, uilabel, trname): self.name = name self.icon = icon self.desc = desc self.uilabel = uilabel self.trname = trname statusTypes = { 'M' : StatusType('modified', 'menucommit.ico', _('%s is modified'), 'status.modified', _('modified')), 'A' : StatusType('added', 'fileadd.ico', _('%s is added'), 'status.added', _('added')), 'R' : StatusType('removed', 'filedelete.ico', _('%s is removed'), 'status.removed', _('removed')), '?' : StatusType('unknown', 'shelve.ico', _('%s is not tracked (unknown)'), 'status.unknown', _('unknown')), '!' : StatusType('deleted', 'menudelete.ico', _('%s is missing!'), 'status.deleted', _('deleted')), 'I' : StatusType('ignored', 'ignore.ico', _('%s is ignored'), 'status.ignored', _('ignored')), 'C' : StatusType('clean', '', _('%s is not modified (clean)'), 'status.clean', _('clean')), 'S' : StatusType('subrepo', 'thg-subrepo.ico', _('%s is a dirty subrepo'), 'status.subrepo', _('subrepo')), } class StatusFilterButton(QToolButton): """Button with drop-down menu for status filter""" statusChanged = pyqtSignal(str) def __init__(self, statustext, types=None, parent=None, **kwargs): self._TYPES = 'MARSC' if types is not None: self._TYPES = types #if 'text' not in kwargs: # kwargs['text'] = _('Status') super(StatusFilterButton, self).__init__( parent, popupMode=QToolButton.MenuButtonPopup, icon=qtlib.geticon('hg-status'), toolButtonStyle=Qt.ToolButtonTextBesideIcon, **kwargs) self.clicked.connect(self.showMenu) self._initactions(statustext) def _initactions(self, text): self._actions = {} menu = QMenu(self) for c in self._TYPES: st = statusTypes[c] a = menu.addAction('%s %s' % (c, st.trname)) a.setCheckable(True) a.setChecked(c in text) a.toggled.connect(self._update) self._actions[c] = a self.setMenu(menu) @pyqtSlot() def _update(self): self.statusChanged.emit(self.status()) def status(self): """Return the text for status filter""" return ''.join(c for c in self._TYPES if self._actions[c].isChecked()) @pyqtSlot(str) def setStatus(self, text): """Set the status text""" assert util.all(c in self._TYPES for c in text) for c in self._TYPES: self._actions[c].setChecked(c in text) class StatusDialog(QDialog): 'Standalone status browser' def __init__(self, repoagent, pats, opts, parent=None): QDialog.__init__(self, parent) self.setWindowIcon(qtlib.geticon('hg-status')) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setContentsMargins(10, 10, 10, 0) self.stwidget = StatusWidget(repoagent, pats, opts, self, checkable=False) toplayout.addWidget(self.stwidget, 1) layout.addLayout(toplayout) self.statusbar = cmdui.ThgStatusBar(self) layout.addWidget(self.statusbar) self.stwidget.showMessage.connect(self.statusbar.showMessage) self.stwidget.progress.connect(self.statusbar.progress) self.stwidget.titleTextChanged.connect(self.setWindowTitle) self.stwidget.linkActivated.connect(self.linkActivated) self._subdialogs = qtlib.DialogKeeper(StatusDialog._createSubDialog, parent=self) self.setWindowTitle(self.stwidget.getTitle()) self.setWindowFlags(Qt.Window) self.loadSettings() qtlib.newshortcutsforstdkey(QKeySequence.Refresh, self, self.stwidget.refreshWctx) QTimer.singleShot(0, self.stwidget.refreshWctx) def linkActivated(self, link): link = unicode(link) if link.startswith('repo:'): self._subdialogs.open(link[len('repo:'):]) def _createSubDialog(self, uroot): # TODO: do not instantiate repo here repo = thgrepo.repository(None, hglib.fromunicode(uroot)) repoagent = repo._pyqtobj return StatusDialog(repoagent, [], {}, parent=self) def loadSettings(self): s = QSettings() self.stwidget.loadSettings(s, 'status') self.restoreGeometry(s.value('status/geom').toByteArray()) def saveSettings(self): s = QSettings() self.stwidget.saveSettings(s, 'status') s.setValue('status/geom', self.saveGeometry()) def accept(self): if not self.stwidget.canExit(): return self.saveSettings() QDialog.accept(self) def reject(self): if not self.stwidget.canExit(): return self.saveSettings() QDialog.reject(self) tortoisehg-2.10/tortoisehg/hgqt/qtapp.py0000644000076400007640000004062712231647662017544 0ustar stevesteve# qtapp.py - utility to start Qt application # # Copyright 2008 Steve Borho # Copyright 2008 TK Soh # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import gc, os, sys, traceback from PyQt4.QtCore import * from PyQt4.QtGui import QApplication, QFont from PyQt4.QtNetwork import QLocalServer, QLocalSocket if PYQT_VERSION < 0x40600 or QT_VERSION < 0x40600: sys.stderr.write('TortoiseHg requires at least Qt 4.6 and PyQt 4.6\n') sys.stderr.write('You have Qt %s and PyQt %s\n' % (QT_VERSION_STR, PYQT_VERSION_STR)) sys.exit(-1) from mercurial import error, util from tortoisehg.hgqt.i18n import _ from tortoisehg.util import hglib, i18n from tortoisehg.util import version as thgversion from tortoisehg.hgqt import bugreport, qtlib, thgrepo, workbench try: from thginithook import thginithook except ImportError: thginithook = None # {exception class: message} # It doesn't check the hierarchy of exception classes for simplicity. _recoverableexc = { error.RepoLookupError: _('Try refreshing your repository.'), error.RevlogError: _('Try refreshing your repository.'), error.ParseError: _('Error string "%(arg0)s" at %(arg1)s
    Please ' 'edit your config'), error.ConfigError: _('Configuration Error: "%(arg0)s",
    Please ' 'fix your config'), error.Abort: _('Operation aborted:

    %(arg0)s.'), error.LockUnavailable: _('Repository is locked'), } def earlyExceptionMsgBox(e): """Show message for recoverable error before the QApplication is started""" opts = {} opts['cmd'] = ' '.join(sys.argv[1:]) opts['values'] = e opts['error'] = traceback.format_exc() opts['nofork'] = True errstring = _recoverableexc[e.__class__] if not QApplication.instance(): main = QApplication(sys.argv) dlg = bugreport.ExceptionMsgBox(hglib.tounicode(str(e)), errstring, opts) dlg.exec_() def earlyBugReport(e): """Show generic errors before the QApplication is started""" opts = {} opts['cmd'] = ' '.join(sys.argv[1:]) opts['error'] = traceback.format_exc() if not QApplication.instance(): main = QApplication(sys.argv) dlg = bugreport.BugReport(opts) dlg.exec_() class ExceptionCatcher(QObject): """Catch unhandled exception raised inside Qt event loop""" _exceptionOccured = pyqtSignal(object, object, object) def __init__(self, ui, mainapp, parent=None): super(ExceptionCatcher, self).__init__(parent) self._ui = ui self._mainapp = mainapp self.errors = [] # can be emitted by another thread; postpones it until next # eventloop of main (GUI) thread. self._exceptionOccured.connect(self.putexception, Qt.QueuedConnection) self._ui.debug('setting up excepthook\n') self._origexcepthook = sys.excepthook sys.excepthook = self.ehook def release(self): if not self._origexcepthook: return self._ui.debug('restoring excepthook\n') sys.excepthook = self._origexcepthook self._origexcepthook = None def ehook(self, etype, evalue, tracebackobj): 'Will be called by any thread, on any unhandled exception' if self._ui.debugflag: elist = traceback.format_exception(etype, evalue, tracebackobj) self._ui.debug(''.join(elist)) self._exceptionOccured.emit(etype, evalue, tracebackobj) # not thread-safe to touch self.errors here @pyqtSlot(object, object, object) def putexception(self, etype, evalue, tracebackobj): 'Enque exception info and display it later; run in main thread' if not self.errors: QTimer.singleShot(10, self.excepthandler) self.errors.append((etype, evalue, tracebackobj)) @pyqtSlot() def excepthandler(self): 'Display exception info; run in main (GUI) thread' try: try: self._showexceptiondialog() except: # make sure to quit mainloop first, so that it never leave # zombie process. self._mainapp.exit(1) self._printexception() finally: self.errors = [] def _showexceptiondialog(self): opts = {} opts['cmd'] = ' '.join(sys.argv[1:]) opts['error'] = ''.join(''.join(traceback.format_exception(*args)) for args in self.errors) etype, evalue = self.errors[0][:2] parent = self._mainapp.activeWindow() if (len(set(e[0] for e in self.errors)) == 1 and etype in _recoverableexc): opts['values'] = evalue errstr = _recoverableexc[etype] if etype is error.Abort and evalue.hint: errstr = u''.join([errstr, u'
    ', _('hint:'), u' %(arg1)s']) opts['values'] = [str(evalue), evalue.hint] dlg = bugreport.ExceptionMsgBox(hglib.tounicode(str(evalue)), errstr, opts, parent=parent) dlg.exec_() elif etype is KeyboardInterrupt: self.errors = [] if qtlib.QuestionMsgBox(_('Keyboard Interrupt'), _('Close this application?'), parent=parent): self._mainapp.exit(-1) else: dlg = bugreport.BugReport(opts, parent=parent) dlg.exec_() def _printexception(self): for args in self.errors: traceback.print_exception(*args) class GarbageCollector(QObject): ''' Disable automatic garbage collection and instead collect manually every INTERVAL milliseconds. This is done to ensure that garbage collection only happens in the GUI thread, as otherwise Qt can crash. ''' INTERVAL = 5000 def __init__(self, ui, parent): QObject.__init__(self, parent) self._ui = ui self.timer = QTimer(self) self.timer.timeout.connect(self.check) self.threshold = gc.get_threshold() gc.disable() self.timer.start(self.INTERVAL) #gc.set_debug(gc.DEBUG_SAVEALL) def check(self): l0, l1, l2 = gc.get_count() if l0 > self.threshold[0]: num = gc.collect(0) self._ui.debug('GarbageCollector.check: %d %d %d\n' % (l0, l1, l2)) self._ui.debug('collected gen 0, found %d unreachable\n' % num) if l1 > self.threshold[1]: num = gc.collect(1) self._ui.debug('collected gen 1, found %d unreachable\n' % num) if l2 > self.threshold[2]: num = gc.collect(2) self._ui.debug('collected gen 2, found %d unreachable\n' % num) def debug_cycles(self): gc.collect() for obj in gc.garbage: self._ui.debug('%s, %r, %s\n' % (obj, obj, type(obj))) def allowSetForegroundWindow(processid=-1): """Allow a given process to set the foreground window""" # processid = -1 means ASFW_ANY (i.e. allow any process) if os.name == 'nt': # on windows we must explicitly allow bringing the main window to # the foreground. To do so we must use ctypes try: from ctypes import windll windll.user32.AllowSetForegroundWindow(processid) except ImportError: pass def connectToExistingWorkbench(root, revset=None): """ Connect and send data to an existing workbench server For the connection to be successful, the server must loopback the data that we send to it. Normally the data that is sent will be a repository root path, but we can also send "echo" to check that the connection works (i.e. that there is a server) """ if revset: data = '\0'.join([root, revset]) else: data = root servername = QApplication.applicationName() + '-' + util.getuser() socket = QLocalSocket() socket.connectToServer(servername, QIODevice.ReadWrite) if socket.waitForConnected(10000): # Momentarily let any process set the foreground window # The server process with revoke this permission as soon as it gets # the request allowSetForegroundWindow() socket.write(QByteArray(data)) socket.flush() socket.waitForReadyRead(10000) reply = socket.readAll() if data == reply: return True elif socket.error() == QLocalSocket.ConnectionRefusedError: # last server process was crashed? QLocalServer.removeServer(servername) return False def _fixapplicationfont(): if os.name != 'nt': return try: import win32gui, win32con except ImportError: return # use configurable font like GTK, Mozilla XUL or Eclipse SWT ncm = win32gui.SystemParametersInfo(win32con.SPI_GETNONCLIENTMETRICS) lf = ncm['lfMessageFont'] f = QFont(hglib.tounicode(lf.lfFaceName)) f.setItalic(lf.lfItalic) if lf.lfWeight != win32con.FW_DONTCARE: weights = [(0, QFont.Light), (400, QFont.Normal), (600, QFont.DemiBold), (700, QFont.Bold), (800, QFont.Black)] n, w = filter(lambda e: e[0] <= lf.lfWeight, weights)[-1] f.setWeight(w) f.setPixelSize(abs(lf.lfHeight)) QApplication.setFont(f, 'QWidget') def _gettranslationpath(): """Return path to Qt's translation file (.qm)""" if getattr(sys, 'frozen', False): return ':/translations' else: return QLibraryInfo.location(QLibraryInfo.TranslationsPath) class QtRunner(QObject): """Run Qt app and hold its windows NOTE: This object will be instantiated before QApplication, it means there's a limitation on Qt's event handling. See http://doc.qt.nokia.com/4.6/threads-qobject.html#per-thread-event-loop """ def __init__(self): super(QtRunner, self).__init__() self._ui = None self._mainapp = None self._exccatcher = None self._server = None self._repomanager = None self._workbench = None self._dialogs = {} # dlg: reporoot def __call__(self, dlgfunc, ui, *args, **opts): if self._mainapp: self._opendialog(dlgfunc, args, opts) return QSettings.setDefaultFormat(QSettings.IniFormat) self._ui = ui self._mainapp = QApplication(sys.argv) self._exccatcher = ExceptionCatcher(ui, self._mainapp, self) self._gc = GarbageCollector(ui, self) # default org is used by QSettings self._mainapp.setApplicationName('TortoiseHgQt') self._mainapp.setOrganizationName('TortoiseHg') self._mainapp.setOrganizationDomain('tortoisehg.org') self._mainapp.setApplicationVersion(thgversion.version()) self._fixlibrarypaths() self._installtranslator() QFont.insertSubstitutions('monospace', ['monaco', 'courier new']) _fixapplicationfont() qtlib.configstyles(ui) qtlib.initfontcache(ui) self._mainapp.setWindowIcon(qtlib.geticon('thg-logo')) self._repomanager = thgrepo.RepoManager(ui, self) dlg, reporoot = self._createdialog(dlgfunc, args, opts) try: if dlg: dlg.show() dlg.raise_() else: return -1 if thginithook is not None: thginithook() return self._mainapp.exec_() finally: if reporoot: self._repomanager.releaseRepoAgent(reporoot) if self._server: self._server.close() self._exccatcher.release() self._mainapp = self._ui = None def _fixlibrarypaths(self): # make sure to use the bundled Qt plugins to avoid ABI incompatibility # http://qt-project.org/doc/qt-4.8/deployment-windows.html#qt-plugins if os.name == 'nt' and getattr(sys, 'frozen', False): self._mainapp.setLibraryPaths([self._mainapp.applicationDirPath()]) def _installtranslator(self): if not i18n.language: return t = QTranslator(self._mainapp) t.load('qt_' + i18n.language, _gettranslationpath()) self._mainapp.installTranslator(t) def _createdialog(self, dlgfunc, args, opts): assert self._ui and self._repomanager reporoot = None try: args = list(args) if 'repository' in opts: repoagent = self._repomanager.openRepoAgent( hglib.tounicode(opts['repository'])) reporoot = repoagent.rootPath() args.insert(0, repoagent) return dlgfunc(self._ui, *args, **opts), reporoot except error.RepoError, inst: qtlib.WarningMsgBox(_('Repository Error'), hglib.tounicode(str(inst))) except error.Abort, inst: qtlib.WarningMsgBox(_('Abort'), hglib.tounicode(str(inst)), hglib.tounicode(inst.hint or '')) if reporoot: self._repomanager.releaseRepoAgent(reporoot) return None, None def _opendialog(self, dlgfunc, args, opts): dlg, reporoot = self._createdialog(dlgfunc, args, opts) if not dlg: return self._dialogs[dlg] = reporoot # avoid garbage collection if hasattr(dlg, 'finished') and hasattr(dlg.finished, 'connect'): dlg.finished.connect(dlg.deleteLater) # NOTE: Somehow `destroyed` signal doesn't emit the original obj. # So we cannot write `dlg.destroyed.connect(self._forgetdialog)`. dlg.destroyed.connect(lambda: self._forgetdialog(dlg)) dlg.show() def _forgetdialog(self, dlg): """forget the dialog to be garbage collectable""" assert dlg in self._dialogs reporoot = self._dialogs.pop(dlg) if reporoot: self._repomanager.releaseRepoAgent(reporoot) def createWorkbench(self): """Create Workbench window and keep single reference""" assert self._ui and self._mainapp and self._repomanager assert not self._workbench self._workbench = workbench.Workbench(self._ui, self._repomanager) return self._workbench def showRepoInWorkbench(self, uroot, rev=-1): """Show the specified repository in Workbench""" assert self._mainapp if not self._workbench: self.createWorkbench() assert self._workbench wb = self._workbench wb.show() wb.activateWindow() wb.raise_() wb.showRepo(uroot) if rev != -1: wb.goto(hglib.fromunicode(uroot), rev) def createWorkbenchServer(self): assert self._mainapp assert not self._server self._server = QLocalServer(self) self._server.newConnection.connect(self._handleNewConnection) self._server.listen(self._mainapp.applicationName() + '-' + util.getuser()) @pyqtSlot() def _handleNewConnection(self): socket = self._server.nextPendingConnection() if socket: socket.waitForReadyRead(10000) data = str(socket.readAll()) if data and data != '[echo]': args = data.split('\0', 1) if len(args) > 1: uroot, urevset = map(hglib.tounicode, args) else: uroot = hglib.tounicode(args[0]) urevset = None self.showRepoInWorkbench(uroot) wb = self._workbench if urevset: wb.setRevsetFilter(uroot, urevset) # Bring the workbench window to the front # This assumes that the client process has # called allowSetForegroundWindow(-1) right before # sending the request wb.setWindowState(wb.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) wb.show() wb.raise_() wb.activateWindow() # Revoke the blanket permission to set the foreground window allowSetForegroundWindow(os.getpid()) socket.write(QByteArray(data)) socket.flush() tortoisehg-2.10/tortoisehg/hgqt/graft.py0000644000076400007640000002527712231647662017526 0ustar stevesteve# graft.py - Graft dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * import os from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, cmdui, resolve, thgrepo, wctxcleaner from tortoisehg.hgqt import csinfo, cslist BB = QDialogButtonBox class GraftDialog(QDialog): showMessage = pyqtSignal(QString) def __init__(self, repoagent, parent, **opts): super(GraftDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('hg-transplant')) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent self._graftstatefile = self.repo.join('graftstate') self.aborted = False self.valid = True def cleanrevlist(revlist): return [self.repo[rev].rev() for rev in revlist] self.sourcelist = cleanrevlist(opts.get('source', ['.'])) currgraftrevs = self.graftstate() if currgraftrevs: currgraftrevs = cleanrevlist(currgraftrevs) if self.sourcelist != currgraftrevs: res = qtlib.CustomPrompt(_('Interrupted graft operation found'), _('An interrupted graft operation has been found.\n\n' 'You cannot perform a different graft operation unless ' 'you abort the interrupted graft operation first.'), self, (_('Continue or abort interrupted graft operation?'), _('Cancel')), 1, 2).run() if res != 0: # Cancel self.valid = False return # Continue creating the dialog, but use the graft source # of the existing, interrupted graft as the source, rather than # the one that was passed as an option to the dialog constructor self.sourcelist = currgraftrevs box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(6,)*4) self.setLayout(box) destrev = self.repo['.'].rev() if len(self.sourcelist) > 1: listlabel = qtlib.LabeledSeparator( _('Graft %d changesets on top of changeset %s') \ % (len(self.sourcelist), destrev)) self.layout().addWidget(listlabel) self.cslist = cslist.ChangesetList(self.repo) self.cslist.update(self.sourcelist) self.layout().addWidget(self.cslist) style = csinfo.panelstyle(selectable=True) self.srcb = srcb = QGroupBox() srcb.setLayout(QVBoxLayout()) srcb.layout().setContentsMargins(*(2,)*4) self.source = csinfo.create(self.repo, None, style, withupdate=True) self._updateSource(0) srcb.layout().addWidget(self.source) self.layout().addWidget(srcb) destb = QGroupBox( _('To graft destination')) destb.setLayout(QVBoxLayout()) destb.layout().setContentsMargins(*(2,)*4) dest = csinfo.create(self.repo, destrev, style, withupdate=True) destb.layout().addWidget(dest) self.destcsinfo = dest self.layout().addWidget(destb) sep = qtlib.LabeledSeparator(_('Options')) self.layout().addWidget(sep) self.currentuservechk = QCheckBox(_('Use my user name instead of graft ' 'committer user name')) self.layout().addWidget(self.currentuservechk) self.currentdatevechk = QCheckBox(_('Use current date')) self.layout().addWidget(self.currentdatevechk) self.logvechk = QCheckBox(_('Append graft info to log message')) self.layout().addWidget(self.logvechk) self.autoresolvechk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) self.autoresolvechk.setChecked( self.repo.ui.configbool('tortoisehg', 'autoresolve', False)) self.layout().addWidget(self.autoresolvechk) self.cmd = cmdui.Widget(True, True, self) self.cmd.commandFinished.connect(self.commandFinished) self.showMessage.connect(self.cmd.stbar.showMessage) self.cmd.stbar.linkActivated.connect(self.linkActivated) self.layout().addWidget(self.cmd, 2) bbox = QDialogButtonBox() self.cancelbtn = bbox.addButton(QDialogButtonBox.Cancel) self.cancelbtn.clicked.connect(self.reject) self.graftbtn = bbox.addButton(_('Graft'), QDialogButtonBox.ActionRole) self.graftbtn.clicked.connect(self.graft) self.abortbtn = bbox.addButton(_('Abort'), QDialogButtonBox.ActionRole) self.abortbtn.clicked.connect(self.abort) self.layout().addWidget(bbox) self.bbox = bbox self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkFinished.connect(self._onCheckFinished) if self.checkResolve(): self.abortbtn.setEnabled(True) else: self.showMessage.emit(_('Checking...')) self.abortbtn.setEnabled(False) self.graftbtn.setEnabled(False) QTimer.singleShot(0, self._wctxcleaner.check) self.setMinimumWidth(480) self.setMaximumHeight(800) self.resize(0, 340) self.setWindowTitle(_('Graft - %s') % self.repo.displayname) @property def repo(self): return self._repoagent.rawRepo() def _updateSourceTitle(self, idx): numrevs = len(self.sourcelist) if numrevs <= 1: title = _('Graft changeset') else: title = _('Graft changeset #%d of %d') % (idx + 1, numrevs) self.srcb.setTitle(title) def _updateSource(self, idx): self._updateSourceTitle(idx) self.source.update(self.repo[self.sourcelist[idx]]) @pyqtSlot(bool) def _onCheckFinished(self, clean): if not clean: self.graftbtn.setEnabled(False) txt = _('Before graft, you must ' 'commit or ' 'discard changes.') else: self.graftbtn.setEnabled(True) txt = _('You may continue or start the graft') self.showMessage.emit(txt) def graft(self): self.graftbtn.setEnabled(False) self.cancelbtn.setShown(False) cmdline = ['graft', '--repository', self.repo.root] cmdline += ['--config', 'ui.merge=internal:' + (self.autoresolvechk.isChecked() and 'merge' or 'fail')] if self.currentuservechk.isChecked(): cmdline += ['--currentuser'] if self.currentdatevechk.isChecked(): cmdline += ['--currentdate'] if self.logvechk.isChecked(): cmdline += ['--log'] if os.path.exists(self._graftstatefile): cmdline += ['--continue'] else: for source in self.sourcelist: cmdline += [str(source)] self.repo.incrementBusyCount() self.cmd.run(cmdline) def abort(self): self.abortbtn.setDisabled(True) if os.path.exists(self._graftstatefile): # Remove the existing graftstate file! os.remove(self._graftstatefile) cmdline = ['update', '--repository', self.repo.root, '--clean', '--rev', 'p1()'] self.repo.incrementBusyCount() self.aborted = True self.cmd.run(cmdline) def graftstate(self): graftstatefile = self.repo.join('graftstate') if os.path.exists(graftstatefile): f = open(graftstatefile, 'r') info = f.readlines() f.close() if len(info): revlist = [rev.strip() for rev in info] revlist = [rev for rev in revlist if rev != ''] if revlist: return revlist return None def commandFinished(self, ret): self.repo.decrementBusyCount() if self.aborted or self.checkResolve() is False: msg = _('Graft is complete') if self.aborted: msg = _('Graft aborted') elif ret == 255: msg = _('Graft failed') self.cmd.setShowOutput(True) # contains hint else: self._updateSource(len(self.sourcelist) - 1) self.showMessage.emit(msg) self.graftbtn.setEnabled(True) self.graftbtn.setText(_('Close')) self.graftbtn.clicked.disconnect(self.graft) self.graftbtn.clicked.connect(self.accept) def checkResolve(self): for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': txt = _('Graft generated merge conflicts that must ' 'be resolved') self.graftbtn.setEnabled(False) break else: self.graftbtn.setEnabled(True) txt = _('You may continue the graft') self.showMessage.emit(txt) currgraftrevs = self.graftstate() if currgraftrevs: def findrev(rev, revlist): rev = self.repo[rev].rev() for n, r in enumerate(revlist): r = self.repo[r].rev() if rev == r: return n return None idx = findrev(currgraftrevs[0], self.sourcelist) if idx is not None: self._updateSource(idx) self.abortbtn.setEnabled(True) self.graftbtn.setText('Continue') return True else: self.abortbtn.setEnabled(False) return False def linkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() self.checkResolve() else: self._wctxcleaner.runCleaner(cmd) def reject(self): if self._wctxcleaner.isChecking(): return if os.path.exists(self._graftstatefile): main = _('Exiting with an unfinished graft is not recommended.') text = _('Consider aborting the graft first.') labels = ((QMessageBox.Yes, _('&Exit')), (QMessageBox.No, _('Cancel'))) if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, labels=labels, parent=self): return super(GraftDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/shellconf.py0000644000076400007640000002346012170335562020363 0ustar stevesteve# shellconf.py - User interface for the TortoiseHg shell extension settings # # Copyright 2009 Steve Borho # Copyright 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import sys from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ from tortoisehg.util import menuthg, hglib from PyQt4.QtCore import * from PyQt4.QtGui import * from _winreg import (HKEY_CURRENT_USER, REG_SZ, REG_DWORD, OpenKey, CreateKey, QueryValueEx, SetValueEx) THGKEY = 'TortoiseHg' OVLKEY = 'TortoiseOverlays' PROMOTEDITEMS = 'PromotedItems' # reading functions def is_true(x): return x in ('1', 'True') def nonzero(x): return x != 0 # writing functions def one_str(x): if x: return '1' return '0' def one_int(x): if x: return 1 return 0 def noop(x): return x vars = { # name: # default, regkey, regtype, evalfunc, wrfunc, checkbuttonattribute 'EnableOverlays': [True, THGKEY, REG_SZ, is_true, one_str, 'ovenable'], 'LocalDisksOnly': [False, THGKEY, REG_SZ, is_true, one_str, 'localonly'], 'ShowTaskbarIcon': [True, THGKEY, REG_SZ, is_true, one_str, 'show_taskbaricon'], 'HighlightTaskbarIcon': [True, THGKEY, REG_SZ, is_true, one_str, 'highlight_taskbaricon'], 'HideMenuOutsideRepo': [False, THGKEY, REG_SZ, is_true, one_str, 'hidecmenu'], PROMOTEDITEMS: ['commit,workbench', THGKEY, REG_SZ, noop, noop, None], 'ShowUnversionedOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableUnversionedHandler'], 'ShowIgnoredOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableIgnoredHandler'], 'ShowLockedOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableLockedHandler'], 'ShowReadonlyOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableReadonlyHandler'], 'ShowDeletedOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableDeletedHandler'], 'ShowAddedOverlay': [True, OVLKEY, REG_DWORD, nonzero, one_int, 'enableAddedHandler'] } class ShellConfigWindow(QDialog): def __init__(self, parent=None): super(ShellConfigWindow, self).__init__(parent) self.menu_cmds = {} self.dirty = False layout = QVBoxLayout() tw = QTabWidget() layout.addWidget(tw) # cmenu tab cmenuwidget = QWidget() grid = QGridLayout() cmenuwidget.setLayout(grid) tw.addTab(cmenuwidget, _("Context Menu")) w = QLabel(_("Top menu items:")) grid.addWidget(w, 0, 0) self.topmenulist = w = QListWidget() grid.addWidget(w, 1, 0, 4, 1) w.itemClicked.connect(self.listItemClicked) w = QLabel(_("Sub menu items:")) grid.addWidget(w, 0, 2) self.submenulist = w = QListWidget() grid.addWidget(w, 1, 2, 4, 1) w.itemClicked.connect(self.listItemClicked) style = QApplication.style() icon = style.standardIcon(QStyle.SP_ArrowLeft) self.top_button = w = QPushButton(icon, '') grid.addWidget(w, 2, 1) w.clicked.connect(self.top_clicked) icon = style.standardIcon(QStyle.SP_ArrowRight) self.sub_button = w = QPushButton(icon, '') grid.addWidget(w, 3, 1) w.clicked.connect(self.sub_clicked) grid.setRowStretch(1, 10) grid.setRowStretch(4, 10) def checkbox(label): cb = QCheckBox(label) cb.stateChanged.connect(self.stateChanged) return cb hidebox = QGroupBox(_('Menu Behavior')) grid.addWidget(hidebox, 5, 0, 5, 3) self.hidecmenu = checkbox(_('Hide context menu outside repositories')) self.hidecmenu.setToolTip(_('Do not show menu items on unversioned ' 'folders (use shift + click to override)')) hidebox.setLayout(QVBoxLayout()) hidebox.layout().addWidget(self.hidecmenu) # Icons tab iconswidget = QWidget() iconslayout = QVBoxLayout() iconswidget.setLayout(iconslayout) tw.addTab(iconswidget, _("Icons")) # Overlays group gbox = QGroupBox(_("Overlays")) iconslayout.addWidget(gbox) hb = QHBoxLayout() gbox.setLayout(hb) self.ovenable = cb = checkbox(_("Enabled overlays")) hb.addWidget(cb) self.localonly = checkbox(_("Local disks only")) hb.addWidget(self.localonly) hb.addStretch() # Enabled Overlay Handlers group gbox = QGroupBox(_("Enabled Overlay Handlers")) iconslayout.addWidget(gbox) grid = QGridLayout() gbox.setLayout(grid) grid.setColumnStretch(3, 10) w = QLabel(_("Warning: affects all Tortoises, logoff required after change")) grid.addWidget(w, 0, 0, 1, 3) self.enableAddedHandler = w = checkbox(_("Added")) grid.addWidget(w, 1, 0) self.enableLockedHandler = w = checkbox(_("Locked*")) grid.addWidget(w, 1, 1) self.enableIgnoredHandler = w = checkbox(_("Ignored*")) grid.addWidget(w, 1, 2) self.enableUnversionedHandler = w = checkbox(_("Unversioned")) grid.addWidget(w, 2, 0) self.enableReadonlyHandler = w = checkbox(_("Readonly*")) grid.addWidget(w, 2, 1) self.enableDeletedHandler = w = checkbox(_("Deleted*")) grid.addWidget(w, 2, 2) w = QLabel(_("*: not used by TortoiseHg")) grid.addWidget(w, 3, 0, 1, 3) # Taskbar group gbox = QGroupBox(_("Taskbar")) iconslayout.addWidget(gbox) hb = QHBoxLayout() gbox.setLayout(hb) self.show_taskbaricon = cb = checkbox(_("Show Icon")) hb.addWidget(cb) self.highlight_taskbaricon = cb = checkbox(_("Highlight Icon")) hb.addWidget(cb) hb.addStretch() iconslayout.addStretch() # dialog buttons BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel|BB.Apply) self.apply_button = bb.button(BB.Apply) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Apply).clicked.connect(self.apply) bb.button(BB.Ok).setDefault(True) layout.addWidget(bb) self.setLayout(layout) self.setWindowTitle(_("TortoiseHg Shell Configuration")) self.setWindowIcon(qtlib.geticon('settings_repo')) self.load_shell_configs() def load_shell_configs(self): for name, info in vars.iteritems(): default, regkey, regtype, evalfunc, wrfunc, cbattr = info try: hkey = OpenKey(HKEY_CURRENT_USER, 'Software\\' + regkey) v = QueryValueEx(hkey, name)[0] vars[name][0] = evalfunc(v) except (WindowsError, EnvironmentError): pass if cbattr != None: checkbutton = getattr(self, cbattr) checkbutton.setChecked(vars[name][0]) promoteditems = vars[PROMOTEDITEMS][0] self.set_menulists(promoteditems) self.dirty = False self.update_states() def set_menulists(self, promoteditems): for list in (self.topmenulist, self.submenulist): list.clear() list.setSortingEnabled(True) promoted = [pi.strip() for pi in promoteditems.split(',')] for cmd, info in menuthg.thgcmenu.items(): label = info['label'] item = QListWidgetItem(hglib.tounicode(label['str'])) item._id = label['id'] if cmd in promoted: self.topmenulist.addItem(item) else: self.submenulist.addItem(item) self.menu_cmds[item._id] = cmd def store_shell_configs(self): if not self.dirty: return promoted = [] list = self.topmenulist for row in range(list.count()): cmd = self.menu_cmds[list.item(row)._id] promoted.append(cmd) hkey = CreateKey(HKEY_CURRENT_USER, "Software\\" + THGKEY) SetValueEx(hkey, PROMOTEDITEMS, 0, REG_SZ, ','.join(promoted)) for name, info in vars.iteritems(): default, regkey, regtype, evalfunc, wrfunc, cbattr = info if cbattr == None: continue checkbutton = getattr(self, cbattr) v = wrfunc(checkbutton.isChecked()) hkey = CreateKey(HKEY_CURRENT_USER, 'Software\\' + regkey) SetValueEx(hkey, name, 0, regtype, v) self.dirty = False self.update_states() def accept(self): self.store_shell_configs() QDialog.accept(self) def reject(self): QDialog.reject(self) def apply(self): self.store_shell_configs() def top_clicked(self): self.move_selected(self.submenulist, self.topmenulist) def sub_clicked(self): self.move_selected(self.topmenulist, self.submenulist) def move_selected(self, fromlist, tolist): row = fromlist.currentRow() if row < 0: return item = fromlist.takeItem(row) tolist.addItem(item) tolist.setCurrentItem(item) fromlist.setCurrentItem(None) self.dirty = True self.update_states() def update_states(self): self.top_button.setEnabled(len(self.submenulist.selectedItems()) > 0) self.sub_button.setEnabled(len(self.topmenulist.selectedItems()) > 0) self.apply_button.setEnabled(self.dirty) def stateChanged(self, state): self.dirty = True self.update_states() def listItemClicked(self, item): itemlist = item.listWidget() for list in (self.topmenulist, self.submenulist): if list != itemlist: list.setCurrentItem(None) self.update_states() tortoisehg-2.10/tortoisehg/hgqt/about.py0000644000076400007640000001552312231647661017525 0ustar stevesteve# about.py - About dialog for TortoiseHg # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuki KODAMA # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. """ TortoiseHg About dialog - PyQt4 version """ import sys from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.util import version, hglib, paths from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest class AboutDialog(QDialog): """Dialog for showing info about TortoiseHg""" def __init__(self, parent=None): super(AboutDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('thg_logo')) self.setWindowTitle(_('About')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.vbox = QVBoxLayout() self.vbox.setSpacing(8) self.logo_lbl = QLabel() self.logo_lbl.setMinimumSize(QSize(92, 50)) self.logo_lbl.setScaledContents(False) self.logo_lbl.setAlignment(Qt.AlignCenter) thglogofile = paths.get_tortoise_icon('thg_logo_92x50.png') self.logo_lbl.setPixmap(QPixmap(thglogofile)) self.vbox.addWidget(self.logo_lbl) self.name_version_libs_lbl = QLabel() self.name_version_libs_lbl.setText(' ') self.name_version_libs_lbl.setAlignment(Qt.AlignCenter) self.name_version_libs_lbl.setTextInteractionFlags( Qt.TextSelectableByMouse) self.vbox.addWidget(self.name_version_libs_lbl) self.getVersionInfo() self.copyright_lbl = QLabel() self.copyright_lbl.setAlignment(Qt.AlignCenter) self.copyright_lbl.setText('\n' + _('Copyright 2008-2013 Steve Borho and others')) self.vbox.addWidget(self.copyright_lbl) self.courtesy_lbl = QLabel() self.courtesy_lbl.setAlignment(Qt.AlignCenter) self.courtesy_lbl.setText( _('Several icons are courtesy of the TortoiseSVN and Tango projects') + '\n') self.vbox.addWidget(self.courtesy_lbl) self.download_url_lbl = QLabel() self.download_url_lbl.setMouseTracking(True) self.download_url_lbl.setAlignment(Qt.AlignCenter) self.download_url_lbl.setTextInteractionFlags(Qt.LinksAccessibleByMouse) self.download_url_lbl.setOpenExternalLinks(True) self.download_url_lbl.setText('%s' % ('http://tortoisehg.org', _('You can visit our site here'))) self.vbox.addWidget(self.download_url_lbl) # Let's have some space between the url and the buttons. self.blancline_lbl = QLabel() self.vbox.addWidget(self.blancline_lbl) self.hbox = QHBoxLayout() self.license_btn = QPushButton() self.license_btn.setText(_('&License')) self.license_btn.setAutoDefault(False) self.license_btn.clicked.connect(self.showLicense) self.hspacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.close_btn = QPushButton() self.close_btn.setText(_('&Close')) self.close_btn.setDefault(True) self.close_btn.clicked.connect(self.close) self.hbox.addWidget(self.license_btn) self.hbox.addItem(self.hspacer) self.hbox.addWidget(self.close_btn) self.vbox.addLayout(self.hbox) self.setLayout(self.vbox) self.layout().setSizeConstraint(QLayout.SetFixedSize) self._readsettings() # Spawn it later, so that the dialog gets visible quickly. QTimer.singleShot(0, self.getUpdateInfo) self._newverreply = None def getVersionInfo(self): def make_version(tuple): vers = ".".join([str(x) for x in tuple]) return vers thgv = (_('version %s') % version.version()) libv = (_('with Mercurial-%s, Python-%s, PyQt-%s, Qt-%s') % \ (hglib.hgversion, make_version(sys.version_info[0:3]), PYQT_VERSION_STR, QT_VERSION_STR)) par = ('

    ' '' '%s

    ') name = (par % (14, 'TortoiseHg')) thgv = (par % (10, thgv)) nvl = ''.join([name, thgv, libv]) self.name_version_libs_lbl.setText(nvl) @pyqtSlot() def getUpdateInfo(self): verurl = 'http://tortoisehg.bitbucket.org/curversion.txt' # If we use QNetworkAcessManager elsewhere, it should be shared # through the application. self._netmanager = QNetworkAccessManager(self) self._newverreply = self._netmanager.get(QNetworkRequest(QUrl(verurl))) self._newverreply.finished.connect(self.uFinished) @pyqtSlot() def uFinished(self): newver = (0,0,0) newverstr = '0.0.0' upgradeurl = '' try: f = self._newverreply.readAll().data().splitlines() self._newverreply.close() self._newverreply = None newverstr = f[0] newver = tuple([int(p) for p in newverstr.split('.')]) upgradeurl = f[1] # generic download URL platform = sys.platform if platform == 'win32': from win32process import IsWow64Process as IsX64 platform = IsX64() and 'x64' or 'x86' # linux2 for Linux, darwin for OSX for line in f[2:]: p, _url = line.split(':', 1) if platform == p: upgradeurl = _url.strip() break except (IndexError, ImportError, ValueError): pass try: thgv = version.version() if '+' in thgv: thgv = thgv[:thgv.index('+')] curver = tuple([int(p) for p in thgv.split('.')]) except ValueError: curver = (0,0,0) if newver > curver: url_lbl = _('A new version of TortoiseHg (%s) ' 'is ready for download!') % newverstr urldata = ('%s' % (upgradeurl, url_lbl)) self.download_url_lbl.setText(urldata) def showLicense(self): from tortoisehg.hgqt import license ld = license.LicenseDialog(self) ld.show() def closeEvent(self, event): if self._newverreply: self._newverreply.abort() self._writesettings() super(AboutDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('about/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('about/geom', self.saveGeometry()) tortoisehg-2.10/tortoisehg/hgqt/webconf.py0000644000076400007640000003074312174032435020030 0ustar stevesteve# webconf.py - Widget to show/edit hgweb config # # Copyright 2010 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.util import hglib, wconfig from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt.webconf_ui import Ui_WebconfForm _FILE_FILTER = ';;'.join([_('Config files (*.conf *.config *.ini)'), _('All files (*)')]) class WebconfForm(QWidget): """Widget to show/edit webconf""" def __init__(self, parent=None, webconf=None): super(WebconfForm, self).__init__(parent, acceptDrops=True) self._qui = Ui_WebconfForm() self._qui.setupUi(self) self._initicons() self._qui.path_edit.currentIndexChanged.connect(self._updateview) self._qui.path_edit.currentIndexChanged.connect(self._updateform) self._qui.add_button.clicked.connect(self._addpathmap) self.setwebconf(webconf or wconfig.config()) self._updateform() def _initicons(self): def setstdicon(w, name): w.setIcon(self.style().standardIcon(name)) setstdicon(self._qui.open_button, QStyle.SP_DialogOpenButton) setstdicon(self._qui.save_button, QStyle.SP_DialogSaveButton) self._qui.add_button.setIcon(qtlib.geticon('fileadd')) self._qui.edit_button.setIcon(qtlib.geticon('edit-file')) self._qui.remove_button.setIcon(qtlib.geticon('filedelete')) def dragEnterEvent(self, event): if self._getlocalpath_from_dropevent(event): event.setDropAction(Qt.LinkAction) event.accept() def dropEvent(self, event): localpath = self._getlocalpath_from_dropevent(event) if localpath: event.setDropAction(Qt.LinkAction) event.accept() self._addpathmap(localpath=localpath) @staticmethod def _getlocalpath_from_dropevent(event): m = event.mimeData() if m.hasFormat('text/uri-list') and len(m.urls()) == 1: return unicode(m.urls()[0].toLocalFile()) def setwebconf(self, webconf): """set current webconf object""" path = hglib.tounicode(getattr(webconf, 'path', None) or '') i = self._qui.path_edit.findText(path) if i < 0: i = 0 self._qui.path_edit.insertItem(i, path, webconf) self._qui.path_edit.setCurrentIndex(i) @property def webconf(self): """current webconf object""" def curconf(w): i = w.currentIndex() _path, conf = unicode(w.itemText(i)), w.itemData(i).toPyObject() return conf return curconf(self._qui.path_edit) @property def _webconfmodel(self): """current model object of webconf""" return self._qui.repos_view.model() @pyqtSlot() def _updateview(self): m = WebconfModel(config=self.webconf, parent=self) self._qui.repos_view.setModel(m) self._qui.repos_view.selectionModel().currentChanged.connect( self._updateform) def _updateform(self): """Update availability of each widget""" self._qui.repos_view.setEnabled(hasattr(self.webconf, 'write')) self._qui.add_button.setEnabled(hasattr(self.webconf, 'write')) self._qui.edit_button.setEnabled( hasattr(self.webconf, 'write') and self._qui.repos_view.currentIndex().isValid()) self._qui.remove_button.setEnabled( hasattr(self.webconf, 'write') and self._qui.repos_view.currentIndex().isValid()) @pyqtSlot() def on_open_button_clicked(self): path = QFileDialog.getOpenFileName( self, _('Open hgweb config'), getattr(self.webconf, 'path', None) or '', _FILE_FILTER) if path: self.openwebconf(path) def openwebconf(self, path): """load the specified webconf file""" path = hglib.fromunicode(path) c = wconfig.readfile(path) c.path = os.path.abspath(path) self.setwebconf(c) @pyqtSlot() def on_save_button_clicked(self): path = QFileDialog.getSaveFileName( self, _('Save hgweb config'), getattr(self.webconf, 'path', None) or '', _FILE_FILTER) if path: self.savewebconf(path) def savewebconf(self, path): """save current webconf to the specified file""" path = hglib.fromunicode(path) wconfig.writefile(self.webconf, path) self.openwebconf(path) # reopen in case file path changed @pyqtSlot() def _addpathmap(self, path=None, localpath=None): path, localpath = _PathDialog.getaddpathmap( self, path=path, localpath=localpath, invalidpaths=self._webconfmodel.paths) if path: self._webconfmodel.addpathmap(path, localpath) @pyqtSlot() def on_edit_button_clicked(self): self.on_repos_view_doubleClicked(self._qui.repos_view.currentIndex()) @pyqtSlot(QModelIndex) def on_repos_view_doubleClicked(self, index): assert index.isValid() origpath, origlocalpath = self._webconfmodel.getpathmapat(index.row()) path, localpath = _PathDialog.geteditpathmap( self, path=origpath, localpath=origlocalpath, invalidpaths=set(self._webconfmodel.paths) - set([origpath])) if not path: return if path != origpath: # we cannot change config key without reordering self._webconfmodel.removepathmap(origpath) self._webconfmodel.addpathmap(path, localpath) else: self._webconfmodel.setpathmap(path, localpath) @pyqtSlot() def on_remove_button_clicked(self): index = self._qui.repos_view.currentIndex() assert index.isValid() path, _localpath = self._webconfmodel.getpathmapat(index.row()) self._webconfmodel.removepathmap(path) class _PathDialog(QDialog): """Dialog to add/edit path mapping""" def __init__(self, title, acceptlabel, path=None, localpath=None, invalidpaths=None, parent=None): super(_PathDialog, self).__init__(parent) self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint) & ~Qt.WindowContextHelpButtonHint) self.resize(QFontMetrics(self.font()).width('M') * 50, self.height()) self.setWindowTitle(title) self._invalidpaths = set(invalidpaths or []) self.setLayout(QFormLayout(fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow)) self._initfields() self._initbuttons(acceptlabel) self._path_edit.setText(path or os.path.basename(localpath or '')) self._localpath_edit.setText(localpath or '') self._updateform() def _initfields(self): """initialize input fields""" def addfield(key, label, *extras): edit = QLineEdit(self) edit.textChanged.connect(self._updateform) if extras: field = QHBoxLayout() field.addWidget(edit) for e in extras: field.addWidget(e) else: field = edit self.layout().addRow(label, field) setattr(self, '_%s_edit' % key, edit) addfield('path', _('Path:')) self._localpath_browse_button = QToolButton( icon=self.style().standardIcon(QStyle.SP_DialogOpenButton)) addfield('localpath', _('Local Path:'), self._localpath_browse_button) self._localpath_browse_button.clicked.connect(self._browse_localpath) def _initbuttons(self, acceptlabel): """initialize dialog buttons""" self._buttons = QDialogButtonBox(self) self._accept_button = self._buttons.addButton(QDialogButtonBox.Ok) self._reject_button = self._buttons.addButton(QDialogButtonBox.Cancel) self._accept_button.setText(acceptlabel) self._buttons.accepted.connect(self.accept) self._buttons.rejected.connect(self.reject) self.layout().addRow(self._buttons) @property def path(self): """value of path field""" return unicode(self._path_edit.text()) @property def localpath(self): """value of localpath field""" return unicode(self._localpath_edit.text()) @pyqtSlot() def _browse_localpath(self): path = QFileDialog.getExistingDirectory(self, _('Select Repository'), self.localpath) if not path: return path = unicode(path) if os.path.exists(os.path.join(path, '.hgsub')): self._localpath_edit.setText(os.path.join(path, '**')) else: self._localpath_edit.setText(path) if not self.path: self._path_edit.setText(os.path.basename(path)) @pyqtSlot() def _updateform(self): """update availability of form elements""" self._accept_button.setEnabled(self._isacceptable()) def _isacceptable(self): return bool(self.path and self.localpath and self.path not in self._invalidpaths) @classmethod def getaddpathmap(cls, parent, path=None, localpath=None, invalidpaths=None): d = cls(title=_('Add Path to Serve'), acceptlabel=_('Add'), path=path, localpath=localpath, invalidpaths=invalidpaths, parent=parent) if d.exec_(): return d.path, d.localpath else: return None, None @classmethod def geteditpathmap(cls, parent, path=None, localpath=None, invalidpaths=None): d = cls(title=_('Edit Path to Serve'), acceptlabel=_('Edit'), path=path, localpath=localpath, invalidpaths=invalidpaths, parent=parent) if d.exec_(): return d.path, d.localpath else: return None, None class WebconfModel(QAbstractTableModel): """Wrapper for webconf object to be a Qt's model object""" _COLUMNS = [(_('Path'),), (_('Local Path'),)] def __init__(self, config, parent=None): super(WebconfModel, self).__init__(parent) self._config = config def data(self, index, role): if not index.isValid(): return None if role == Qt.DisplayRole: v = self._config.items('paths')[index.row()][index.column()] return hglib.tounicode(v) return None def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self._config['paths']) def columnCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self._COLUMNS) def headerData(self, section, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None return self._COLUMNS[section][0] @property def paths(self): """return list of known paths""" return [hglib.tounicode(e) for e in self._config['paths']] def getpathmapat(self, row): """return pair of (path, localpath) at the specified index""" assert 0 <= row and row < self.rowCount() return tuple(hglib.tounicode(e) for e in self._config.items('paths')[row]) def addpathmap(self, path, localpath): """add path mapping to serve""" assert path not in self.paths self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) try: self._config.set('paths', hglib.fromunicode(path), hglib.fromunicode(localpath)) finally: self.endInsertRows() def setpathmap(self, path, localpath): """change path mapping at the specified index""" self._config.set('paths', hglib.fromunicode(path), hglib.fromunicode(localpath)) row = self._indexofpath(path) self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount())) def removepathmap(self, path): """remove path from mapping""" row = self._indexofpath(path) self.beginRemoveRows(QModelIndex(), row, row) try: del self._config['paths'][hglib.fromunicode(path)] finally: self.endRemoveRows() def _indexofpath(self, path): path = hglib.fromunicode(path) assert path in self._config['paths'] return list(self._config['paths']).index(path) tortoisehg-2.10/tortoisehg/hgqt/lexers.py0000644000076400007640000001753012223167342017707 0ustar stevesteve# lexers.py - select Qsci lexer for a filename and contents # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import re from PyQt4 import Qsci, QtGui from tortoisehg.hgqt import qtlib if hasattr(QtGui.QColor, 'getHslF'): def _fixdarkcolors(lexer): """Invert lightness of low-contrast colors on dark theme""" if not qtlib.isdarktheme(): return # fast path # QsciLexer defines 128 styles by default for style in xrange(128): h, s, l, a = lexer.color(style).getHslF() pl = lexer.paper(style).lightnessF() if abs(l - pl) < 0.2: lexer.setColor(QtGui.QColor.fromHslF(h, s, 1.0 - l, a), style) else: # no support for PyQt 4.6.x def _fixdarkcolors(lexer): pass class _LexerSelector(object): _lexer = None def match(self, filename, filedata): return False def lexer(self, parent): """ Return a configured instance of the lexer """ return self.cfg_lexer(self._lexer(parent)) def cfg_lexer(self, lexer): font = qtlib.getfont('fontlog').font() lexer.setFont(font, -1) _fixdarkcolors(lexer) return lexer class _FilenameLexerSelector(_LexerSelector): """ Base class for lexer selector based on file name matching """ extensions = () def match(self, filename, filedata): filename = filename.lower() for ext in self.extensions: if filename.endswith(ext): return True return False class _ScriptLexerSelector(_FilenameLexerSelector): """ Base class for lexer selector based on content pattern matching """ regex = None headersize = 3 def match(self, filename, filedata): if super(_ScriptLexerSelector, self).match(filename, filedata): return True if self.regex and filedata: for line in filedata.splitlines()[:self.headersize]: if len(line)<1000 and self.regex.match(line): return True return False class PythonLexerSelector(_ScriptLexerSelector): extensions = ('.py', '.pyw') _lexer = Qsci.QsciLexerPython regex = re.compile(r'^#[!].*python') class BashLexerSelector(_ScriptLexerSelector): extensions = ('.sh', '.bash') _lexer = Qsci.QsciLexerBash regex = re.compile(r'^#[!].*sh') class PerlLexerSelector(_ScriptLexerSelector): extensions = ('.pl', '.perl') _lexer = Qsci.QsciLexerPerl regex = re.compile(r'^#[!].*perl') class RubyLexerSelector(_ScriptLexerSelector): extensions = ('.rb', '.ruby') _lexer = Qsci.QsciLexerRuby regex = re.compile(r'^#[!].*ruby') class LuaLexerSelector(_ScriptLexerSelector): extensions = ('.lua', ) _lexer = Qsci.QsciLexerLua regex = None class _LexerCPP(Qsci.QsciLexerCPP): def refreshProperties(self): super(_LexerCPP, self).refreshProperties() # disable grey-out of inactive block, which is hard to read. # as of QScintilla 2.7.2, this property isn't mapped to wrapper. self.propertyChanged.emit('lexer.cpp.track.preprocessor', '0') class CppLexerSelector(_FilenameLexerSelector): extensions = ('.c', '.cpp', '.cc', '.cxx', '.cl', '.cu', '.h', '.hpp', '.hh', '.hxx') _lexer = _LexerCPP class DLexerSelector(_FilenameLexerSelector): extensions = ('.d',) _lexer = Qsci.QsciLexerD class PascalLexerSelector(_FilenameLexerSelector): extensions = ('.pas',) _lexer = Qsci.QsciLexerPascal class CSSLexerSelector(_FilenameLexerSelector): extensions = ('.css',) _lexer = Qsci.QsciLexerCSS class XMLLexerSelector(_FilenameLexerSelector): extensions = ('.xhtml', '.xml', '.csproj', 'app.config', 'web.config') _lexer = Qsci.QsciLexerXML class HTMLLexerSelector(_FilenameLexerSelector): extensions = ('.htm', '.html') _lexer = Qsci.QsciLexerHTML class YAMLLexerSelector(_FilenameLexerSelector): extensions = ('.yml',) _lexer = Qsci.QsciLexerYAML class VHDLLexerSelector(_FilenameLexerSelector): extensions = ('.vhd', '.vhdl') _lexer = Qsci.QsciLexerVHDL class BatchLexerSelector(_FilenameLexerSelector): extensions = ('.cmd', '.bat') _lexer = Qsci.QsciLexerBatch class MakeLexerSelector(_FilenameLexerSelector): extensions = ('.mk', 'makefile') _lexer = Qsci.QsciLexerMakefile class CMakeLexerSelector(_FilenameLexerSelector): extensions = ('.cmake', 'cmakelists.txt') _lexer = Qsci.QsciLexerCMake class SQLLexerSelector(_FilenameLexerSelector): extensions = ('.sql',) _lexer = Qsci.QsciLexerSQL class JSLexerSelector(_FilenameLexerSelector): extensions = ('.js', '.json') _lexer = Qsci.QsciLexerJavaScript class JavaLexerSelector(_FilenameLexerSelector): extensions = ('.java',) _lexer = Qsci.QsciLexerJava class TeXLexerSelector(_FilenameLexerSelector): extensions = ('.tex', '.latex',) _lexer = Qsci.QsciLexerTeX class CSharpLexerSelector(_FilenameLexerSelector): extensions = ('.cs',) _lexer = Qsci.QsciLexerCSharp class TCLLexerSelector(_FilenameLexerSelector): extensions = ('.tcl', '.do', '.fdo', '.udo') _lexer = Qsci.QsciLexerTCL class MatlabLexerSelector(_FilenameLexerSelector): extensions = ('.m',) try: _lexer = Qsci.QsciLexerMatlab except AttributeError: # QScintilla<2.5.1 # Python lexer is quite similar _lexer = Qsci.QsciLexerPython class FortranLexerSelector(_FilenameLexerSelector): extensions = ('.f90', '.f95', '.f03',) _lexer = Qsci.QsciLexerFortran class Fortran77LexerSelector(_FilenameLexerSelector): extensions = ('.f', '.f77',) _lexer = Qsci.QsciLexerFortran77 class SpiceLexerSelector(_FilenameLexerSelector): extensions = ('.cir', '.sp',) try: _lexer = Qsci.QsciLexerSpice except AttributeError: # is there a better fallback? _lexer = Qsci.QsciLexerCPP class VerilogLexerSelector(_FilenameLexerSelector): extensions = ('.v', '.vh') try: _lexer = Qsci.QsciLexerVerilog except AttributeError: # is there a better fallback? _lexer = Qsci.QsciLexerCPP class PropertyLexerSelector(_FilenameLexerSelector): extensions = ('.ini', '.properties') _lexer = Qsci.QsciLexerProperties class DiffLexerSelector(_ScriptLexerSelector): extensions = () _lexer = Qsci.QsciLexerDiff regex = re.compile(r'^@@ [-]\d+,\d+ [+]\d+,\d+ @@$') def cfg_lexer(self, lexer): for label, i in (('diff.inserted', 6), ('diff.deleted', 5), ('diff.hunk', 4)): effect = qtlib.geteffect(label) for e in effect.split(';'): if e.startswith('color:'): lexer.setColor(QtGui.QColor(e[7:]), i) if e.startswith('background-color:'): lexer.setEolFill(True, i) lexer.setPaper(QtGui.QColor(e[18:]), i) font = qtlib.getfont('fontdiff').font() lexer.setFont(font, -1) _fixdarkcolors(lexer) return lexer lexers = [] for clsname, cls in globals().items(): if clsname.startswith('_'): continue if isinstance(cls, type) and issubclass(cls, _LexerSelector): lexers.append(cls()) def difflexer(parent): return DiffLexerSelector().lexer(parent) def getlexer(ui, filename, filedata, parent): _, ext = os.path.splitext(filename) if ext and len(ext) > 1: ext = ext.lower()[1:] pref = ui.config('thg-lexer', ext) if pref: lexer = getattr(Qsci, 'QsciLexer' + pref) if lexer and isinstance(lexer, type): return lexer(parent) for lselector in lexers: if lselector.match(filename, filedata): return lselector.lexer(parent) return None tortoisehg-2.10/tortoisehg/hgqt/rename.py0000644000076400007640000002751512231647662017667 0ustar stevesteve# rename.py - TortoiseHg's dialogs for handling renames # # Copyright 2009 Steve Borho # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os, sys from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util, scmutil from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, qtlib, manifestmodel from tortoisehg.util import hglib class RenameDialog(QDialog): """TortoiseHg rename dialog""" output = pyqtSignal(QString, QString) makeLogVisible = pyqtSignal(bool) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repoagent, pats, parent=None, iscopy=False): super(RenameDialog, self).__init__(parent) self._repoagent = repoagent self.iscopy = iscopy # pats: local; src, dest: unicode src, dest = self.init_data(pats) self.init_view(src, dest) def init_data(self, pats): """calculate initial values for widgets""" fname = '' target = '' root = self.repo.root cwd = os.getcwd() try: fname = scmutil.canonpath(root, cwd, pats[0]) target = scmutil.canonpath(root, cwd, pats[1]) except: pass os.chdir(root) fname = hglib.tounicode(util.normpath(fname)) if target: target = hglib.tounicode(util.normpath(target)) else: target = fname return (fname, target) def init_view(self, src, dest): """define the view""" # widgets self.src_lbl = QLabel(_('Source:')) self.src_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.src_txt = QLineEdit(src) self.src_txt.setMinimumWidth(300) self.src_btn = QPushButton(_('Browse...')) self.dest_lbl = QLabel(_('Destination:')) self.dest_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.dest_txt = QLineEdit(dest) self.dest_btn = QPushButton(_('Browse...')) self.copy_chk = QCheckBox(_('Copy source -> destination')) comp = manifestmodel.ManifestCompleter(self) comp.setModel(manifestmodel.ManifestModel(self.repo, parent=comp)) self.src_txt.setCompleter(comp) self.dest_txt.setCompleter(comp) # some extras self.dummy_lbl = QLabel('') self.hgcmd_lbl = QLabel(_('Hg command:')) self.hgcmd_lbl.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) self.show_command(self.compose_command(self.get_src(), self.get_dest())) self.keep_open_chk = QCheckBox(_('Always show output')) # command widget self.cmd = cmdui.Widget(True, False, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandCanceling.connect(self.command_canceling) self.cmd.output.connect(self.output) self.cmd.makeLogVisible.connect(self.makeLogVisible) self.cmd.progress.connect(self.progress) self.cmd.setHidden(True) # bottom buttons self.rename_btn = QPushButton('') self.rename_btn.setDefault(True) self.rename_btn.setFocus(True) self.close_btn = QPushButton(_('&Close')) self.close_btn.setAutoDefault(False) self.detail_btn = QPushButton(_('&Detail')) self.detail_btn.setAutoDefault(False) self.detail_btn.setHidden(True) self.cancel_btn = QPushButton(_('Cancel')) self.cancel_btn.setAutoDefault(False) self.cancel_btn.setHidden(True) # connecting slots self.src_txt.textChanged.connect(self.src_dest_edited) self.src_btn.clicked.connect(self.src_btn_clicked) self.dest_txt.textChanged.connect(self.src_dest_edited) self.dest_btn.clicked.connect(self.dest_btn_clicked) self.copy_chk.toggled.connect(self.copy_chk_toggled) self.rename_btn.clicked.connect(self.rename) self.detail_btn.clicked.connect(self.detail_clicked) self.close_btn.clicked.connect(self.close) self.cancel_btn.clicked.connect(self.cancel_clicked) # main layout self.grid = QGridLayout() self.grid.setSpacing(6) self.grid.addWidget(self.src_lbl, 0, 0) self.grid.addWidget(self.src_txt, 0, 1) self.grid.addWidget(self.src_btn, 0, 2) self.grid.addWidget(self.dest_lbl, 1, 0) self.grid.addWidget(self.dest_txt, 1, 1) self.grid.addWidget(self.dest_btn, 1, 2) self.grid.addWidget(self.copy_chk, 2, 1) self.grid.addWidget(self.dummy_lbl, 3, 1) self.grid.addWidget(self.hgcmd_lbl, 4, 0) self.grid.addWidget(self.hgcmd_txt, 4, 1) self.grid.addWidget(self.keep_open_chk, 5, 1) self.hbox = QHBoxLayout() self.hbox.addWidget(self.detail_btn) self.hbox.addStretch(0) self.hbox.addWidget(self.rename_btn) self.hbox.addWidget(self.close_btn) self.hbox.addWidget(self.cancel_btn) self.vbox = QVBoxLayout() self.vbox.setSpacing(6) self.vbox.addLayout(self.grid) self.vbox.addWidget(self.cmd) self.vbox.addLayout(self.hbox) # dialog setting self.setWindowIcon(qtlib.geticon('hg-rename')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.copy_chk.setChecked(self.iscopy) self.setLayout(self.vbox) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.dest_txt.setFocus() self._readsettings() self.setRenameCopy() def setRenameCopy(self): if self.windowTitle() == '': self.reponame = self.repo.displayname if self.copy_chk.isChecked(): wt = (_('Copy - %s') % self.reponame) self.msgTitle = _('Copy') self.errTitle = _('Copy Error') else: wt = (_('Rename - %s') % self.reponame) self.msgTitle = _('Rename') self.errTitle = _('Rename Error') self.rename_btn.setText(self.msgTitle) self.setWindowTitle(wt) @property def repo(self): return self._repoagent.rawRepo() def get_src(self): return hglib.fromunicode(self.src_txt.text()) def get_dest(self): return hglib.fromunicode(self.dest_txt.text()) def src_dest_edited(self): self.show_command(self.compose_command(self.get_src(), self.get_dest())) def src_btn_clicked(self): """Select the source file of folder""" FD = QFileDialog curr = self.get_src() if os.path.isfile(curr): caption = _('Select Source File') path = FD.getOpenFileName(parent=self, caption=caption, options=FD.ReadOnly) else: caption = _('Select Source Folder') path = FD.getExistingDirectory(parent=self, caption=caption, options=FD.ShowDirsOnly | FD.ReadOnly) relpath = self.to_relative_path(path) if not relpath: return self.src_txt.setText(relpath) def dest_btn_clicked(self): """Select the destination file of folder""" FD = QFileDialog if os.path.isfile(self.get_src()): caption = _('Select Destination File') else: caption = _('Select Destination Folder') path = FD.getSaveFileName(parent=self, caption=caption) relpath = self.to_relative_path(path) if not relpath: return self.dest_txt.setText(relpath) def to_relative_path(self, fullpath): # unicode or QString if not fullpath: return fullpath = util.normpath(unicode(fullpath)) pathprefix = util.normpath(hglib.tounicode(self.repo.root)) + '/' if not os.path.normcase(fullpath).startswith(os.path.normcase(pathprefix)): return return fullpath[len(pathprefix):] def copy_chk_toggled(self): self.setRenameCopy() self.show_command(self.compose_command(self.get_src(), self.get_dest())) def isCaseFoldingOnWin(self): fullsrc = os.path.abspath(self.get_src()) fulldest = os.path.abspath(self.get_dest()) return (fullsrc.upper() == fulldest.upper() and sys.platform == 'win32') def compose_command(self, src, dest): 'src and dest are expected to be in local encoding' if self.copy_chk.isChecked(): cmdline = ['copy'] else: cmdline = ['rename'] cmdline += ['-R', self.repo.root] cmdline.append('-vf') cmdline.append(src) cmdline.append(dest) return cmdline def show_command(self, cmdline): self.hgcmd_txt.setText(hglib.tounicode('hg ' + ' '.join(cmdline))) def rename(self): """execute the rename""" # check inputs src = self.get_src() dest = self.get_dest() if not os.path.exists(src): qtlib.WarningMsgBox(self.msgTitle, _('Source does not exists.')) return fullsrc = os.path.abspath(src) if not fullsrc.startswith(self.repo.root): qtlib.ErrorMsgBox(self.errTitle, _('The source must be within the repository tree.')) return fulldest = os.path.abspath(dest) if not fulldest.startswith(self.repo.root): qtlib.ErrorMsgBox(self.errTitle, _('The destination must be within the repository tree.')) return if src == dest: qtlib.ErrorMsgBox(self.errTitle, _('Please give a destination that differs from the source')) return if (os.path.isfile(dest) and not self.isCaseFoldingOnWin()): res = qtlib.QuestionMsgBox(self.msgTitle, '

    %s

    %s

    ' % (_('Destination file already exists.'), _('Are you sure you want to overwrite it ?')), defaultbutton=QMessageBox.No) if not res: return if self.isCaseFoldingOnWin() and self.copy_chk.isChecked(): qtlib.ErrorMsgBox(self.errTitle, _('Cannot do a pure casefolding copy on Windows')) return cmdline = self.compose_command(src, dest) self.show_command(cmdline) self.cmd.run(cmdline) def detail_clicked(self): if self.cmd.outputShown(): self.cmd.setShowOutput(False) else: self.cmd.setShowOutput(True) def cancel_clicked(self): self.cmd.cancel() def command_started(self): self.src_txt.setEnabled(False) self.src_btn.setEnabled(False) self.dest_txt.setEnabled(False) self.dest_btn.setEnabled(False) self.cmd.setShown(True) self.rename_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) def command_finished(self, ret): if (ret != 0 or self.cmd.outputShown() or self.keep_open_chk.isChecked()): if not self.cmd.outputShown(): self.detail_btn.click() self.cancel_btn.setHidden(True) self.close_btn.setShown(True) self.close_btn.setAutoDefault(True) self.close_btn.setFocus() else: self.reject() def command_canceling(self): self.cancel_btn.setDisabled(True) def closeEvent(self, event): self._writesettings() super(RenameDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('rename/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('rename/geom', self.saveGeometry()) tortoisehg-2.10/tortoisehg/hgqt/commit.py0000644000076400007640000015437112231647662017711 0ustar stevesteve# commit.py - TortoiseHg's commit widget and standalone dialog # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import re import tempfile from mercurial import util, error, scmutil, phases from tortoisehg.util import hglib, shlib, wconfig from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt.messageentry import MessageEntry from tortoisehg.hgqt import cmdcore, cmdui, thgrepo from tortoisehg.hgqt import qtlib, qscilib, status, branchop, revpanel from tortoisehg.hgqt import hgrcutil, mqutil, lfprompt, i18n, partialcommit from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciAPIs if os.name == 'nt': from tortoisehg.util import bugtraq _hasbugtraq = True else: _hasbugtraq = False def readopts(ui): opts = {} opts['ciexclude'] = ui.config('tortoisehg', 'ciexclude', '') opts['pushafter'] = ui.config('tortoisehg', 'cipushafter', '') opts['autoinc'] = ui.config('tortoisehg', 'autoinc', '') opts['recurseinsubrepos'] = ui.config('tortoisehg', 'recurseinsubrepos') opts['bugtraqplugin'] = ui.config('tortoisehg', 'issue.bugtraqplugin') opts['bugtraqparameters'] = ui.config('tortoisehg', 'issue.bugtraqparameters') if opts['bugtraqparameters']: opts['bugtraqparameters'] = os.path.expandvars( opts['bugtraqparameters']) opts['bugtraqtrigger'] = ui.config('tortoisehg', 'issue.bugtraqtrigger') return opts def commitopts2str(opts, mode='commit'): optslist = [] for opt, value in opts.iteritems(): if opt in ['user', 'date', 'pushafter', 'autoinc', 'recurseinsubrepos']: if mode == 'merge' and opt == 'autoinc': # autoinc does not apply to merge commits continue if value is True: optslist.append('--' + opt) elif value: optslist.append('--%s=%s' % (opt, value)) return ' '.join(optslist) _topicmap = { 'amend': _('Commit', 'start progress'), 'commit': _('Commit', 'start progress'), 'qnew': _('MQ Action', 'start progress'), 'qref': _('MQ Action', 'start progress'), 'rollback': _('Rollback', 'start progress'), } # Technical Debt for CommitWidget # disable commit button while no message is entered or no files are selected # qtlib decode failure dialog (ask for retry locale, suggest HGENCODING) # spell check / tab completion # in-memory patching / committing chunk selected files class CommitWidget(QWidget, qtlib.TaskWidget): 'A widget that encompasses a StatusWidget and commit extras' commitButtonEnable = pyqtSignal(bool) linkActivated = pyqtSignal(QString) showMessage = pyqtSignal(unicode) grepRequested = pyqtSignal(unicode, dict) runCustomCommandRequested = pyqtSignal(str, list) commitComplete = pyqtSignal() progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repoagent, pats, opts, parent=None, rev=None): QWidget.__init__(self, parent) repoagent.configChanged.connect(self.refresh) repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.workingBranchChanged.connect(self.refresh) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self._rev = rev self.lastAction = None # Dictionary storing the last "commit messages" # 'commit' is used for 'commit' and 'qnew', while # 'amend' is used for 'amend' and 'qrefresh' self.lastCommitMsgs = {'commit': '', 'amend': ''} self.currentAction = None self.opts = opts = readopts(repo.ui) # user, date self.stwidget = status.StatusWidget(repoagent, pats, opts, self) self.stwidget.showMessage.connect(self.showMessage) self.stwidget.progress.connect(self.progress) self.stwidget.linkActivated.connect(self.linkActivated) self.stwidget.fileDisplayed.connect(self.fileDisplayed) self.stwidget.grepRequested.connect(self.grepRequested) self.stwidget.runCustomCommandRequested.connect( self.runCustomCommandRequested) self.msghistory = [] layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(0) layout.addWidget(self.stwidget) self.setLayout(layout) vbox = QVBoxLayout() vbox.setMargin(0) vbox.setSpacing(0) vbox.setContentsMargins(*(0,)*4) hbox = QHBoxLayout() hbox.setMargin(0) hbox.setContentsMargins(*(0,)*4) tbar = QToolBar(_("Commit Dialog Toolbar"), self) tbar.setStyleSheet(qtlib.tbstylesheet) hbox.addWidget(tbar) self.branchbutton = tbar.addAction(_('Branch: ')) font = self.branchbutton.font() font.setBold(True) self.branchbutton.setFont(font) self.branchbutton.triggered.connect(self.branchOp) self.branchop = None self.recentMessagesButton = QToolButton( text=_('Copy message'), popupMode=QToolButton.MenuButtonPopup, toolTip=_('Copy one of the recent commit messages')) self.recentMessagesButton.clicked.connect( self.recentMessagesButton.showMenu) tbar.addWidget(self.recentMessagesButton) self.updateRecentMessages() tbar.addAction(_('Options')).triggered.connect(self.details) tbar.setIconSize(QSize(16,16)) if _hasbugtraq and self.opts['bugtraqplugin'] != None: # We create the "Show Issues" button, but we delay its setup # because creating the bugtraq object is slow and blocks the GUI, # which would result in a noticeable slow down while creating # the commit widget self.showIssues = tbar.addAction(_('Show Issues')) self.showIssues.setEnabled(False) self.showIssues.setToolTip(_('Please wait...')) def setupBugTraqButton(): self.bugtraq = self.createBugTracker() try: parameters = self.opts['bugtraqparameters'] linktext = self.bugtraq.get_link_text(parameters) except Exception, e: tracker = self.opts['bugtraqplugin'].split(' ', 1)[1] errormsg = _('Failed to load issue tracker \'%s\': %s') \ % (tracker, hglib.tounicode(str(e))) self.showIssues.setToolTip(errormsg) qtlib.ErrorMsgBox(_('Issue Tracker'), errormsg, parent=self) self.bugtraq = None else: # connect UI because we have a valid bug tracker self.commitComplete.connect(self.bugTrackerPostCommit) self.showIssues.setText(linktext) self.showIssues.triggered.connect( self.getBugTrackerCommitMessage) self.showIssues.setToolTip(_('Show Issues...')) self.showIssues.setEnabled(True) QTimer.singleShot(100, setupBugTraqButton) self.stopAction = tbar.addAction(_('Stop')) self.stopAction.triggered.connect(self.stop) self.stopAction.setIcon(qtlib.geticon('process-stop')) self.stopAction.setEnabled(False) hbox.addStretch(1) vbox.addLayout(hbox, 0) self.buttonHBox = hbox if 'mq' in self.repo.extensions(): self.hasmqbutton = True pnhbox = QHBoxLayout() self.pnlabel = QLabel() pnhbox.addWidget(self.pnlabel) self.pnedit = mqutil.getPatchNameLineEdit() self.pnedit.setMaximumWidth(250) pnhbox.addWidget(self.pnedit) pnhbox.addStretch() vbox.addLayout(pnhbox) else: self.hasmqbutton = False self.optionslabel = QLabel() self.optionslabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) vbox.addWidget(self.optionslabel, 0) self.pcsinfo = revpanel.ParentWidget(repo) vbox.addWidget(self.pcsinfo, 0) msgte = MessageEntry(self, self.stwidget.getChecked) msgte.installEventFilter(qscilib.KeyPressInterceptor(self)) vbox.addWidget(msgte, 1) upperframe = QFrame() SP = QSizePolicy sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(1) upperframe.setSizePolicy(sp) upperframe.setLayout(vbox) self.split = QSplitter(Qt.Vertical) if os.name == 'nt': self.split.setStyle(QStyleFactory.create('Plastique')) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(0) self.split.setSizePolicy(sp) # Add our widgets to the top of our splitter self.split.addWidget(upperframe) self.split.setCollapsible(0, False) # Add status widget document frame below our splitter # this reparents the docf from the status splitter self.split.addWidget(self.stwidget.docf) # add our splitter where the docf used to be self.stwidget.split.addWidget(self.split) self.msgte = msgte @property def repo(self): return self._repoagent.rawRepo() @property def rev(self): """Return current revision""" return self._rev def selectRev(self, rev): """ Select the revision that must be set when the dialog is shown again """ self._rev = rev @pyqtSlot(int) @pyqtSlot(object) def setRev(self, rev): """Change revision to show""" self.selectRev(rev) if self.hasmqbutton: preferredActionName = self._getPreferredActionName() curractionName = self.mqgroup.checkedAction()._name if curractionName != preferredActionName: self.commitSetAction(refresh=True, actionName=preferredActionName) def _getPreferredActionName(self): """Select the preferred action, depending on the selected revision""" if not self.hasmqbutton: return 'commit' else: pctx = self.repo.changectx('.') ispatch = 'qtip' in pctx.tags() if not ispatch: # Set the button to Commit return 'commit' elif self.rev is None: # Set the button to QNew return 'qnew' else: # Set the button to QRefresh return 'qref' def commitSetupButton(self): ispatch = lambda r: 'qtip' in r.changectx('.').tags() notpatch = lambda r: 'qtip' not in r.changectx('.').tags() def canamend(r): if ispatch(r): return False ctx = r.changectx('.') return (ctx.phase() != phases.public) \ and len(r.changectx(None).parents()) < 2 \ and not ctx.children() acts = [ ('commit', _('Commit changes'), _('Commit'), notpatch), ('amend', _('Amend current revision'), _('Amend'), canamend), ] if self.hasmqbutton: acts += [ ('qnew', _('Create a new patch'), _('QNew'), None), ('qref', _('Refresh current patch'), _('QRefresh'), ispatch), ] acts = tuple(acts) class CommitToolButton(QToolButton): def styleOption(self): opt = QStyleOptionToolButton() opt.initFrom(self) return opt def menuButtonWidth(self): style = self.style() opt = self.styleOption() opt.features = QStyleOptionToolButton.MenuButtonPopup rect = style.subControlRect(QStyle.CC_ToolButton, opt, QStyle.SC_ToolButtonMenu, self) return rect.width() def setBold(self): f = self.font() f.setWeight(QFont.Bold) self.setFont(f) def sizeHint(self): # Set the desired width to keep the button from resizing return QSize(self._width, QToolButton.sizeHint(self).height()) self.committb = committb = CommitToolButton(self) committb.setBold() committb.setPopupMode(QToolButton.MenuButtonPopup) fmk = lambda s: committb.fontMetrics().width(hglib.tounicode(s[2])) committb._width = max(map(fmk, acts)) + 4*committb.menuButtonWidth() class CommitButtonMenu(QMenu): def __init__(self, parent, repo): self.repo = repo return QMenu.__init__(self, parent) def getActionByName(self, act): return [a for a in self.actions() if a._name == act][0] def showEvent(self, event): for a in self.actions(): if a._enablefunc: a.setEnabled(a._enablefunc(self.repo)) return QMenu.showEvent(self, event) self.mqgroup = QActionGroup(self) commitbmenu = CommitButtonMenu(committb, self.repo) menurefresh = lambda: self.commitSetAction(refresh=True) for a in acts: action = QAction(a[1], self.mqgroup) action._name = a[0] action._text = a[2] action._enablefunc = a[3] action.triggered.connect(menurefresh) action.setCheckable(True) commitbmenu.addAction(action) committb.setMenu(commitbmenu) committb.clicked.connect(self.mqPerformAction) self.commitButtonEnable.connect(committb.setEnabled) self.commitSetAction(actionName=self._getPreferredActionName()) sc = QShortcut(QKeySequence('Ctrl+Return'), self, self.mqPerformAction) sc.setContext(Qt.WidgetWithChildrenShortcut) sc = QShortcut(QKeySequence('Ctrl+Enter'), self, self.mqPerformAction) sc.setContext(Qt.WidgetWithChildrenShortcut) return committb @pyqtSlot(bool) def commitSetAction(self, refresh=False, actionName=None): allowcs = False if actionName: selectedAction = \ [act for act in self.mqgroup.actions() \ if act._name == actionName][0] selectedAction.setChecked(True) curraction = self.mqgroup.checkedAction() oldpctx = self.stwidget.pctx pctx = self.repo.changectx('.') if curraction._name == 'qnew': self.pnlabel.setVisible(True) self.pnedit.setVisible(True) self.pnedit.setFocus() self.pnedit.setText(mqutil.defaultNewPatchName(self.repo)) self.pnedit.selectAll() self.stwidget.setPatchContext(None) refreshwctx = refresh and oldpctx is not None else: if self.hasmqbutton: self.pnlabel.setVisible(False) self.pnedit.setVisible(False) ispatch = 'qtip' in pctx.tags() def switchAction(action, name): action.setChecked(False) action = self.committb.menu().getActionByName(name) action.setChecked(True) return action if curraction._name == 'qref' and not ispatch: curraction = switchAction(curraction, 'commit') elif curraction._name == 'commit' and ispatch: curraction = switchAction(curraction, 'qref') if curraction._name in ('qref', 'amend'): refreshwctx = refresh self.stwidget.setPatchContext(pctx) elif curraction._name == 'commit': refreshwctx = refresh and oldpctx is not None self.stwidget.setPatchContext(None) allowcs = len(self.repo.parents()) == 1 if curraction._name in ('qref', 'amend'): if self.lastAction not in ('qref', 'amend'): self.lastCommitMsgs['commit'] = self.msgte.text() if self.lastCommitMsgs['amend']: msg = self.lastCommitMsgs['amend'] else: msg = hglib.tounicode(pctx.description()) self.setMessage(msg) else: if self.lastAction in ('qref', 'amend'): self.lastCommitMsgs['amend'] = self.msgte.text() self.setMessage(self.lastCommitMsgs['commit']) if curraction._name == 'amend': self.stwidget.defcheck = 'amend' else: self.stwidget.defcheck = 'commit' self.stwidget.fileview.enableChangeSelection(allowcs) if not allowcs: self.stwidget.partials = {} if refreshwctx: self.stwidget.refreshWctx() self.committb.setText(curraction._text) self.lastAction = curraction._name def getBranchCommandLine(self): ''' Create the command line to change or create the selected branch unless it is the selected branch Verify whether a branch exists on a repo. If it doesn't ask the user to confirm that it wants to create the branch. If it does and it is not the current branch as the user whether it wants to change to that branch. Depending on the user input, create the command line which will perform the selected action ''' # This function is used both by commit() and mqPerformAction() repo = self.repo commandlines = [] newbranch = False branch = hglib.fromunicode(self.branchop) if branch in repo.branchtags(): # response: 0=Yes, 1=No, 2=Cancel if branch in [p.branch() for p in repo.parents()]: resp = 0 else: rev = repo[branch].rev() resp = qtlib.CustomPrompt(_('Confirm Branch Change'), _('Named branch "%s" already exists, ' 'last used in revision %d\n' ) % (self.branchop, rev), self, (_('Restart &Branch'), _('&Commit to current branch'), _('Cancel')), 2, 2).run() else: resp = qtlib.CustomPrompt(_('Confirm New Branch'), _('Create new named branch "%s" with this commit?\n' ) % self.branchop, self, (_('Create &Branch'), _('&Commit to current branch'), _('Cancel')), 2, 2).run() if resp == 0: newbranch = True commandlines.append(['branch', '--force', branch]) elif resp == 2: return None, False return commandlines, newbranch @pyqtSlot() def mqPerformAction(self): curraction = self.mqgroup.checkedAction() if curraction._name == 'commit': return self.commit() elif curraction._name == 'amend': return self.commit(amend=True) # Check if we need to change branch first wholecmdlines = [] # [[cmd1, ...], [cmd2, ...], ...] if self.branchop: cmdlines, newbranch = self.getBranchCommandLine() if cmdlines is None: return wholecmdlines.extend(cmdlines) olist = ('user', 'date') cmdlines = mqutil.mqNewRefreshCommand(self.repo, curraction._name == 'qnew', self.stwidget, self.pnedit, self.msgte.text(), self.opts, olist) if not cmdlines: return wholecmdlines.extend(cmdlines) self._runCommand(curraction._name, wholecmdlines) @pyqtSlot(QString, QString) def fileDisplayed(self, wfile, contents): 'Status widget is displaying a new file' if not (wfile and contents): return if self.msgte.autoCompletionThreshold() < 0: # do not search for tokens if auto completion is disabled # pygments has several infinite loop problems we'd like to avoid return if self.msgte.lexer() is None: # qsci will crash if None is passed to QsciAPIs constructor return wfile = unicode(wfile) self._apis = QsciAPIs(self.msgte.lexer()) tokens = set() for e in self.stwidget.getChecked(): e = hglib.tounicode(e) tokens.add(e) tokens.add(os.path.basename(e)) tokens.add(wfile) tokens.add(os.path.basename(wfile)) try: from pygments.lexers import guess_lexer_for_filename from pygments.token import Token from pygments.util import ClassNotFound try: contents = unicode(contents) lexer = guess_lexer_for_filename(wfile, contents) for tokentype, value in lexer.get_tokens(contents): if tokentype in Token.Name and len(value) > 4: tokens.add(value) except ClassNotFound, TypeError: pass except ImportError: pass for n in sorted(list(tokens)): self._apis.add(n) self._apis.apiPreparationFinished.connect(self.apiPrepFinished) self._apis.prepare() def apiPrepFinished(self): 'QsciAPIs has finished parsing displayed file' self.msgte.lexer().setAPIs(self._apis) def bugTrackerPostCommit(self): if not _hasbugtraq or self.opts['bugtraqtrigger'] != 'commit': return # commit already happened, get last message in history message = self.lastmessage error = self.bugtraq.on_commit_finished(message) if error != None and len(error) > 0: qtlib.ErrorMsgBox(_('Issue Tracker'), error, parent=self) # recreate bug tracker to get new COM object for next commit self.bugtraq = self.createBugTracker() def createBugTracker(self): bugtraqid = self.opts['bugtraqplugin'].split(' ', 1)[0] result = bugtraq.BugTraq(bugtraqid) return result def getBugTrackerCommitMessage(self): parameters = self.opts['bugtraqparameters'] message = self.getMessage(True) newMessage = self.bugtraq.get_commit_message(parameters, message) self.setMessage(newMessage) def details(self): mode = 'commit' if len(self.repo.parents()) > 1: mode = 'merge' dlg = DetailsDialog(self.opts, self.userhist, self, mode=mode) dlg.finished.connect(dlg.deleteLater) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self.opts.update(dlg.outopts) self.refresh() @pyqtSlot() def repositoryChanged(self): 'Repository has detected a changelog / dirstate change' # clear the last 'amend' message # do not clear the last 'commit' message because there are many cases # in which we may write a commit message first, modify the repository # (e.g. amend or update and merge uncommitted changes) and then do the # actual commit self.lastCommitMsgs['amend'] = '' self.refresh() self.stwidget.refreshWctx() # Trigger reload of working context @pyqtSlot() def refreshWctx(self): 'User has requested a working context refresh' self.stwidget.refreshWctx() # Trigger reload of working context @pyqtSlot() def reload(self): 'User has requested a reload' self.repo.thginvalidate() self.refresh() self.stwidget.refreshWctx() # Trigger reload of working context @pyqtSlot() def refresh(self): ispatch = self.repo.changectx('.').thgmqappliedpatch() if not self.hasmqbutton: self.commitButtonEnable.emit(not ispatch) self.msgte.refresh(self.repo) # Update branch operation button branchu = hglib.tounicode(self.repo[None].branch()) if self.branchop is None: title = _('Branch: ') + branchu elif self.branchop == False: title = _('Close Branch: ') + branchu else: title = _('New Branch: ') + self.branchop self.branchbutton.setText(title) # Update options label, showing only whitelisted options. opts = commitopts2str(self.opts) self.optionslabelfmt = _('Selected Options: %s') self.optionslabel.setText(self.optionslabelfmt % Qt.escape(hglib.tounicode(opts))) self.optionslabel.setVisible(bool(opts)) # Update parent csinfo widget self.pcsinfo.set_revision(None) self.pcsinfo.update() # This is ugly, but want pnlabel to have the same alignment/style/etc # as pcsinfo, so extract the needed parts of pcsinfo's markup. Would # be nicer if csinfo exposed this information, or if csinfo could hold # widgets like pnlabel. if self.hasmqbutton: parent = _('Parent:') patchname = _('Patch name:') text = unicode(self.pcsinfo.revlabel.text()) cellend = '' firstidx = text.find(cellend) + len(cellend) secondidx = text[firstidx:].rfind('') if firstidx >= 0 and secondidx >= 0: start = text[0:firstidx].replace(parent, patchname) self.pnlabel.setText(start + text[firstidx+secondidx:]) else: self.pnlabel.setText(patchname) self.commitSetAction() def branchOp(self): d = branchop.BranchOpDialog(self.repo, self.branchop, self) d.setWindowFlags(Qt.Sheet) d.setWindowModality(Qt.WindowModal) if d.exec_() == QDialog.Accepted: self.branchop = d.branchop if self.branchop is False: if not self.getMessage(True).strip(): engmsg = self.repo.ui.configbool( 'tortoisehg', 'engmsg', False) msgset = i18n.keepgettext()._('Close %s branch') text = engmsg and msgset['id'] or msgset['str'] self.setMessage(unicode(text) % hglib.tounicode(self.repo[None].branch())) self.refresh() def canUndo(self): 'Returns undo description or None if not valid' if os.path.exists(self.repo.sjoin('undo')): try: args = self.repo.opener('undo.desc', 'r').read().splitlines() if args[1] != 'commit': return None return _('Rollback commit to revision %d') % (int(args[0]) - 1) except (IOError, IndexError, ValueError): pass return None def rollback(self): msg = self.canUndo() if not msg: return d = QMessageBox.question(self, _('Confirm Undo'), msg, QMessageBox.Ok | QMessageBox.Cancel) if d != QMessageBox.Ok: return self._runCommand('rollback', [['rollback']]) def updateRecentMessages(self): # Define a menu that lists recent messages m = QMenu(self.recentMessagesButton) for s in self.msghistory: title = s.split('\n', 1)[0][:70] def overwriteMsg(newMsg): return lambda: self.msgSelected(newMsg) m.addAction(title).triggered.connect(overwriteMsg(s)) self.recentMessagesButton.setMenu(m) def getMessage(self, allowreplace): text = self.msgte.text() try: return hglib.fromunicode(text, 'strict') except UnicodeEncodeError: if allowreplace: return hglib.fromunicode(text, 'replace') else: raise def msgSelected(self, message): if self.msgte.text() and self.msgte.isModified(): d = QMessageBox.question(self, _('Confirm Discard Message'), _('Discard current commit message?'), QMessageBox.Ok | QMessageBox.Cancel) if d != QMessageBox.Ok: return self.setMessage(message) self.msgte.setFocus() def setMessage(self, msg): self.msgte.setText(msg) self.msgte.moveCursorToEnd() self.msgte.setModified(False) def canExit(self): if not self.stwidget.canExit(): return False return self._cmdsession.isFinished() def loadSettings(self, s, prefix): 'Load history, etc, from QSettings instance' repoid = str(self.repo[0]) lpref = prefix + '/commit/' # local settings (splitter, etc) gpref = 'commit/' # global settings (history, etc) # message history is stored in unicode self.split.restoreState(s.value(lpref+'split').toByteArray()) self.msgte.loadSettings(s, lpref+'msgte') self.stwidget.loadSettings(s, lpref+'status') self.msghistory = list(s.value(gpref+'history-'+repoid).toStringList()) self.msghistory = [unicode(m) for m in self.msghistory if m] self.updateRecentMessages() self.userhist = map(unicode, s.value(gpref+'userhist').toStringList()) self.userhist = [u for u in self.userhist if u] try: curmsg = self.repo.opener('cur-message.txt').read() self.setMessage(hglib.tounicode(curmsg)) except EnvironmentError: pass try: curmsg = self.repo.opener('last-message.txt').read() if curmsg: self.addMessageToHistory(hglib.tounicode(curmsg)) except EnvironmentError: pass def saveSettings(self, s, prefix): 'Save history, etc, in QSettings instance' try: repoid = str(self.repo[0]) lpref = prefix + '/commit/' gpref = 'commit/' s.setValue(lpref+'split', self.split.saveState()) self.msgte.saveSettings(s, lpref+'msgte') self.stwidget.saveSettings(s, lpref+'status') s.setValue(gpref+'history-'+repoid, self.msghistory) s.setValue(gpref+'userhist', self.userhist) msg = self.getMessage(True) self.repo.opener('cur-message.txt', 'w').write(msg) except (EnvironmentError, IOError): pass def addMessageToHistory(self, umsg): umsg = unicode(umsg) if umsg in self.msghistory: self.msghistory.remove(umsg) self.msghistory.insert(0, umsg) self.msghistory = self.msghistory[:10] self.updateRecentMessages() def addUsernameToHistory(self, user): user = hglib.tounicode(user) if user in self.userhist: self.userhist.remove(user) self.userhist.insert(0, user) self.userhist = self.userhist[:10] def commit(self, amend=False): repo = self.repo try: msg = self.getMessage(False) except UnicodeEncodeError: res = qtlib.CustomPrompt( _('Message Translation Failure'), _('Unable to translate message to local encoding\n' 'Consider setting HGENCODING environment variable\n' 'Replace untranslatable characters with "?"?\n'), self, (_('&Replace'), _('Cancel')), 0, 1, []).run() if res == 0: msg = self.getMessage(True) msg = str(msg) # drop round-trip utf8 data self.msgte.setText(hglib.tounicode(msg)) self.msgte.setFocus() return if not msg: qtlib.WarningMsgBox(_('Nothing Commited'), _('Please enter commit message'), parent=self) self.msgte.setFocus() return linkmandatory = self.repo.ui.configbool('tortoisehg', 'issue.linkmandatory', False) if linkmandatory: issueregex = self.repo.ui.config('tortoisehg', 'issue.regex') if issueregex: m = re.search(issueregex, msg) if not m: qtlib.WarningMsgBox(_('Nothing Commited'), _('No issue link was found in the ' 'commit message. The commit message ' 'should contain an issue link. ' "Configure this in the 'Issue " "Tracking' section of the settings."), parent=self) self.msgte.setFocus() return False commandlines = [] brcmd = [] newbranch = False if self.branchop is None: newbranch = repo[None].branch() != repo['.'].branch() elif self.branchop == False: brcmd = ['--close-branch'] else: commandlines, newbranch = self.getBranchCommandLine() if commandlines is None: return partials = [] if len(repo.parents()) > 1: merge = True self.files = [] else: merge = False files = self.stwidget.getChecked('MAR?!S') # make list of files with partial change selections for fname, c in self.stwidget.partials.iteritems(): if c.excludecount > 0 and c.excludecount < len(c.hunks): partials.append(fname) self.files = set(files + partials) canemptycommit = bool(brcmd or newbranch or amend) if not (self.files or canemptycommit or merge): qtlib.WarningMsgBox(_('No files checked'), _('No modified files checkmarked for commit'), parent=self) self.stwidget.tv.setFocus() return user = qtlib.getCurrentUsername(self, self.repo, self.opts) if not user: return self.addUsernameToHistory(user) checkedUnknowns = self.stwidget.getChecked('?I') if checkedUnknowns: confirm = self.repo.ui.configbool('tortoisehg', 'confirmaddfiles', True) if confirm: res = qtlib.CustomPrompt( _('Confirm Add'), _('Add selected untracked files?'), self, (_('&Add'), _('Cancel')), 0, 1, checkedUnknowns).run() else: res = 0 if res == 0: haslf = 'largefiles' in repo.extensions() if haslf: result = lfprompt.promptForLfiles(self, repo.ui, repo, checkedUnknowns) if not result: return checkedUnknowns, lfiles = result if lfiles: cmd = ['add', '--large'] + \ [repo.wjoin(f) for f in lfiles] commandlines.append(cmd) cmd = ['add'] + [repo.wjoin(f) for f in checkedUnknowns] commandlines.append(cmd) else: return checkedMissing = self.stwidget.getChecked('!') if checkedMissing: confirm = self.repo.ui.configbool('tortoisehg', 'confirmdeletefiles', True) if confirm: res = qtlib.CustomPrompt( _('Confirm Remove'), _('Remove selected deleted files?'), self, (_('&Remove'), _('Cancel')), 0, 1, checkedMissing).run() else: res = 0 if res == 0: cmd = ['remove'] + [repo.wjoin(f) for f in checkedMissing] commandlines.append(cmd) else: return try: date = self.opts.get('date') if date: util.parsedate(date) dcmd = ['--date', date] else: dcmd = [] except error.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) self.showMessage.emit(err) dcmd = [] cmdline = ['commit', '--verbose', '--user', user, '--message='+msg] cmdline += dcmd + brcmd if partials: partialcommit.uisetup(repo.ui) # write patch for partial change selections to temp file fd, tmpname = tempfile.mkstemp(prefix='thg-patch-') fp = os.fdopen(fd, 'wb') for fname in partials: changes = self.stwidget.partials[fname] changes.write(fp) for chunk in changes.hunks: if not chunk.excluded: chunk.write(fp) fp.close() cmdline.append('--partials') cmdline.append(tmpname) assert not amend if self.opts.get('recurseinsubrepos'): cmdline.append('--subrepos') if amend: cmdline.append('--amend') if not self.files and canemptycommit and not merge: # make sure to commit empty changeset by excluding all files cmdline.extend(['--exclude', repo.root]) assert not self.stwidget.partials cmdline.append('--') cmdline.extend([repo.wjoin(f) for f in self.files]) if len(repo.parents()) == 1: for fname in self.opts.get('autoinc', '').split(','): fname = fname.strip() if fname: cmdline.append(repo.wjoin(fname)) commandlines.append(cmdline) if self.opts.get('pushafter'): cmd = ['push', self.opts['pushafter']] if newbranch: cmd.append('--new-branch') commandlines.append(cmd) self._runCommand(amend and 'amend' or 'commit', commandlines) def stop(self): self._cmdsession.abort() def _runCommand(self, action, cmdlines): self.currentAction = action self.progress.emit(*cmdui.startProgress(_topicmap[action], '')) self.commitButtonEnable.emit(False) ucmdlines = [map(hglib.tounicode, xs) for xs in cmdlines] self._cmdsession = sess = self._repoagent.runCommandSequence(ucmdlines, self) sess.commandFinished.connect(self.commandFinished) def commandFinished(self, ret): self.progress.emit(*cmdui.stopProgress(_topicmap[self.currentAction])) self.stopAction.setEnabled(False) self.commitButtonEnable.emit(True) if ret == 0: self.stwidget.partials = {} if self.currentAction == 'rollback': shlib.shell_notify([self.repo.root]) return self.branchop = None umsg = self.msgte.text() if self.currentAction not in ('qref', 'amend'): self.lastCommitMsgs['commit'] = '' if self.currentAction == 'commit': # capture last message for BugTraq plugin self.lastmessage = self.getMessage(True) if umsg: self.addMessageToHistory(umsg) self.setMessage('') if self.currentAction == 'commit': shlib.shell_notify(self.files) self.commitComplete.emit() else: cmdui.errorMessageBox(self._cmdsession, self, _('Commit', 'window title')) class DetailsDialog(QDialog): 'Utility dialog for configuring uncommon settings' def __init__(self, opts, userhistory, parent, mode='commit'): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - commit options') % parent.repo.displayname) self.repo = parent.repo layout = QVBoxLayout() self.setLayout(layout) hbox = QHBoxLayout() self.usercb = QCheckBox(_('Set username:')) usercombo = QComboBox() usercombo.setEditable(True) usercombo.setEnabled(False) SP = QSizePolicy usercombo.setSizePolicy(SP(SP.Expanding, SP.Minimum)) self.usercb.toggled.connect(usercombo.setEnabled) self.usercb.toggled.connect(lambda s: s and usercombo.setFocus()) l = [] if opts.get('user'): val = hglib.tounicode(opts['user']) self.usercb.setChecked(True) l.append(val) try: val = hglib.tounicode(self.repo.ui.username()) l.append(val) except util.Abort: pass for name in userhistory: if name not in l: l.append(name) for name in l: usercombo.addItem(name) self.usercombo = usercombo usersaverepo = QPushButton(_('Save in Repo')) usersaverepo.clicked.connect(self.saveInRepo) usersaverepo.setEnabled(False) self.usercb.toggled.connect(usersaverepo.setEnabled) usersaveglobal = QPushButton(_('Save Global')) usersaveglobal.clicked.connect(self.saveGlobal) usersaveglobal.setEnabled(False) self.usercb.toggled.connect(usersaveglobal.setEnabled) hbox.addWidget(self.usercb) hbox.addWidget(self.usercombo) hbox.addWidget(usersaverepo) hbox.addWidget(usersaveglobal) layout.addLayout(hbox) hbox = QHBoxLayout() self.datecb = QCheckBox(_('Set Date:')) self.datele = QLineEdit() self.datele.setEnabled(False) self.datecb.toggled.connect(self.datele.setEnabled) curdate = QPushButton(_('Update')) curdate.setEnabled(False) self.datecb.toggled.connect(curdate.setEnabled) self.datecb.toggled.connect(lambda s: s and curdate.setFocus()) curdate.clicked.connect( lambda: self.datele.setText( hglib.tounicode(hglib.displaytime(util.makedate())))) if opts.get('date'): self.datele.setText(opts['date']) self.datecb.setChecked(True) else: self.datecb.setChecked(False) curdate.clicked.emit(True) hbox.addWidget(self.datecb) hbox.addWidget(self.datele) hbox.addWidget(curdate) layout.addLayout(hbox) hbox = QHBoxLayout() self.pushaftercb = QCheckBox(_('Push After Commit:')) self.pushafterle = QLineEdit() self.pushafterle.setEnabled(False) self.pushaftercb.toggled.connect(self.pushafterle.setEnabled) self.pushaftercb.toggled.connect(lambda s: s and self.pushafterle.setFocus()) pushaftersave = QPushButton(_('Save in Repo')) pushaftersave.clicked.connect(self.savePushAfter) pushaftersave.setEnabled(False) self.pushaftercb.toggled.connect(pushaftersave.setEnabled) if opts.get('pushafter'): val = hglib.tounicode(opts['pushafter']) self.pushafterle.setText(val) self.pushaftercb.setChecked(True) hbox.addWidget(self.pushaftercb) hbox.addWidget(self.pushafterle) hbox.addWidget(pushaftersave) layout.addLayout(hbox) hbox = QHBoxLayout() self.autoinccb = QCheckBox(_('Auto Includes:')) self.autoincle = QLineEdit() self.autoincle.setEnabled(False) self.autoinccb.toggled.connect(self.autoincle.setEnabled) self.autoinccb.toggled.connect(lambda s: s and self.autoincle.setFocus()) autoincsave = QPushButton(_('Save in Repo')) autoincsave.clicked.connect(self.saveAutoInc) autoincsave.setEnabled(False) self.autoinccb.toggled.connect(autoincsave.setEnabled) if opts.get('autoinc'): val = hglib.tounicode(opts['autoinc']) self.autoincle.setText(val) self.autoinccb.setChecked(True) hbox.addWidget(self.autoinccb) hbox.addWidget(self.autoincle) hbox.addWidget(autoincsave) if mode != 'merge': #self.autoinccb.setVisible(False) layout.addLayout(hbox) hbox = QHBoxLayout() recursesave = QPushButton(_('Save in Repo')) recursesave.clicked.connect(self.saveRecurseInSubrepos) self.recursecb = QCheckBox(_('Recurse into subrepositories ' '(--subrepos)')) SP = QSizePolicy self.recursecb.setSizePolicy(SP(SP.Expanding, SP.Minimum)) #self.recursecb.toggled.connect(recursesave.setEnabled) if opts.get('recurseinsubrepos'): self.recursecb.setChecked(True) hbox.addWidget(self.recursecb) hbox.addWidget(recursesave) layout.addLayout(hbox) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.bb = bb layout.addWidget(bb) def saveInRepo(self): fn = os.path.join(self.repo.root, '.hg', 'hgrc') self.saveToPath([fn]) def saveGlobal(self): self.saveToPath(scmutil.userrcpath()) def saveToPath(self, path): fn, cfg = hgrcutil.loadIniFile(path, self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save username'), _('Iniparse must be installed.'), parent=self) return if fn is None: return try: user = hglib.fromunicode(self.usercombo.currentText()) if user: cfg.set('ui', 'username', user) else: try: del cfg['ui']['username'] except KeyError: pass wconfig.writefile(cfg, fn) except IOError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(e), parent=self) def savePushAfter(self): path = os.path.join(self.repo.root, '.hg', 'hgrc') fn, cfg = hgrcutil.loadIniFile([path], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save after commit push'), _('Iniparse must be installed.'), parent=self) return if fn is None: return try: remote = hglib.fromunicode(self.pushafterle.text()) if remote: cfg.set('tortoisehg', 'cipushafter', remote) else: try: del cfg['tortoisehg']['cipushafter'] except KeyError: pass wconfig.writefile(cfg, fn) except IOError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(e), parent=self) def saveAutoInc(self): path = os.path.join(self.repo.root, '.hg', 'hgrc') fn, cfg = hgrcutil.loadIniFile([path], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save auto include list'), _('Iniparse must be installed.'), parent=self) return if fn is None: return try: list = hglib.fromunicode(self.autoincle.text()) if list: cfg.set('tortoisehg', 'autoinc', list) else: try: del cfg['tortoisehg']['autoinc'] except KeyError: pass wconfig.writefile(cfg, fn) except IOError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(e), parent=self) def saveRecurseInSubrepos(self): path = os.path.join(self.repo.root, '.hg', 'hgrc') fn, cfg = hgrcutil.loadIniFile([path], self) if not hasattr(cfg, 'write'): qtlib.WarningMsgBox(_('Unable to save recurse in subrepos.'), _('Iniparse must be installed.'), parent=self) return if fn is None: return try: state = self.recursecb.isChecked() if state: cfg.set('tortoisehg', 'recurseinsubrepos', state) else: try: del cfg['tortoisehg']['recurseinsubrepos'] except KeyError: pass wconfig.writefile(cfg, fn) except IOError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(e), parent=self) def accept(self): outopts = {} if self.datecb.isChecked(): date = hglib.fromunicode(self.datele.text()) try: util.parsedate(date) except error.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) qtlib.WarningMsgBox(_('Invalid date format'), err, parent=self) return outopts['date'] = date else: outopts['date'] = '' if self.usercb.isChecked(): user = hglib.fromunicode(self.usercombo.currentText()) else: user = '' outopts['user'] = user if not user: try: self.repo.ui.username() except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) qtlib.WarningMsgBox(_('No username configured'), err, parent=self) return if self.pushaftercb.isChecked(): remote = hglib.fromunicode(self.pushafterle.text()) outopts['pushafter'] = remote else: outopts['pushafter'] = '' if self.autoinccb.isChecked(): outopts['autoinc'] = hglib.fromunicode(self.autoincle.text()) else: outopts['autoinc'] = '' if self.recursecb.isChecked(): outopts['recurseinsubrepos'] = 'true' else: outopts['recurseinsubrepos'] = '' self.outopts = outopts QDialog.accept(self) class CommitDialog(QDialog): 'Standalone commit tool, a wrapper for CommitWidget' def __init__(self, repoagent, pats, opts, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon('hg-commit')) self.pats = pats self.opts = opts layout = QVBoxLayout() layout.setMargin(0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setContentsMargins(5, 5, 5, 0) layout.addLayout(toplayout) commit = CommitWidget(repoagent, pats, opts, self, rev='.') toplayout.addWidget(commit, 1) self.statusbar = cmdui.ThgStatusBar(self) commit.showMessage.connect(self.statusbar.showMessage) commit.progress.connect(self.statusbar.progress) commit.linkActivated.connect(self.linkActivated) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Close|BB.Discard) bb.rejected.connect(self.reject) bb.button(BB.Discard).setText('Undo') bb.button(BB.Discard).clicked.connect(commit.rollback) bb.button(BB.Close).setDefault(False) bb.button(BB.Discard).setDefault(False) self.commitButton = commit.commitSetupButton() bb.addButton(self.commitButton, BB.AcceptRole) self.bb = bb toplayout.addWidget(self.bb) layout.addWidget(self.statusbar) self._subdialogs = qtlib.DialogKeeper(CommitDialog._createSubDialog, parent=self) s = QSettings() self.restoreGeometry(s.value('commit/geom').toByteArray()) commit.loadSettings(s, 'committool') repoagent.repositoryChanged.connect(self.updateUndo) commit.commitComplete.connect(self.postcommit) repo = repoagent.rawRepo() self.setWindowTitle(_('%s - commit') % repo.displayname) self.commit = commit self.commit.reload() self.updateUndo() self.commit.msgte.setFocus() qtlib.newshortcutsforstdkey(QKeySequence.Refresh, self, self.refresh) def linkActivated(self, link): link = unicode(link) if link.startswith('repo:'): self._subdialogs.open(link[len('repo:'):]) def _createSubDialog(self, uroot): # TODO: do not instantiate repo here repo = thgrepo.repository(None, hglib.fromunicode(uroot)) repoagent = repo._pyqtobj return CommitDialog(repoagent, [], {}, parent=self) @pyqtSlot() def updateUndo(self): BB = QDialogButtonBox undomsg = self.commit.canUndo() if undomsg: self.bb.button(BB.Discard).setEnabled(True) self.bb.button(BB.Discard).setToolTip(undomsg) else: self.bb.button(BB.Discard).setEnabled(False) self.bb.button(BB.Discard).setToolTip('') def refresh(self): self.updateUndo() self.commit.reload() def postcommit(self): repo = self.commit.stwidget.repo if repo.ui.configbool('tortoisehg', 'closeci'): if self.commit.canExit(): self.reject() else: self.commit.stwidget.refthread.wait() QTimer.singleShot(0, self.reject) def promptExit(self): exit = self.commit.canExit() if not exit: exit = qtlib.QuestionMsgBox(_('TortoiseHg Commit'), _('Are you sure that you want to cancel the commit operation?'), parent=self) if exit: s = QSettings() s.setValue('commit/geom', self.saveGeometry()) self.commit.saveSettings(s, 'committool') return exit def accept(self): self.commit.commit() def reject(self): if self.promptExit(): QDialog.reject(self) tortoisehg-2.10/tortoisehg/hgqt/rebase.py0000644000076400007640000002265112231647662017655 0ustar stevesteve# rebase.py - Rebase dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * import os from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, csinfo, cmdui, resolve, thgrepo, wctxcleaner BB = QDialogButtonBox class RebaseDialog(QDialog): showMessage = pyqtSignal(QString) def __init__(self, repoagent, parent, **opts): super(RebaseDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('hg-rebase')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() self.opts = opts self.aborted = False box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(6,)*4) self.setLayout(box) style = csinfo.panelstyle(selectable=True) srcb = QGroupBox( _('Rebase changeset and descendants')) srcb.setLayout(QVBoxLayout()) srcb.layout().setContentsMargins(*(2,)*4) s = opts.get('source', '.') source = csinfo.create(self.repo, s, style, withupdate=True) srcb.layout().addWidget(source) self.sourcecsinfo = source self.layout().addWidget(srcb) destb = QGroupBox( _('To rebase destination')) destb.setLayout(QVBoxLayout()) destb.layout().setContentsMargins(*(2,)*4) d = opts.get('dest', '.') dest = csinfo.create(self.repo, d, style, withupdate=True) destb.layout().addWidget(dest) self.destcsinfo = dest self.layout().addWidget(destb) swaplabel = QLabel('%s' # don't care href % _('Swap source and destination')) swaplabel.linkActivated.connect(self.swap) self.layout().addWidget(swaplabel) sep = qtlib.LabeledSeparator(_('Options')) self.layout().addWidget(sep) self.keepchk = QCheckBox(_('Keep original changesets')) self.keepchk.setChecked(opts.get('keep', False)) self.layout().addWidget(self.keepchk) self.keepbrancheschk = QCheckBox(_('Keep original branch names')) self.keepbrancheschk.setChecked(opts.get('keepbranches', False)) self.layout().addWidget(self.keepbrancheschk) self.collapsechk = QCheckBox(_('Collapse the rebased changesets ')) self.collapsechk.setChecked(opts.get('collapse', False)) self.layout().addWidget(self.collapsechk) self.basechk = QCheckBox(_('Rebase entire source branch')) self.layout().addWidget(self.basechk) self.autoresolvechk = QCheckBox(_('Automatically resolve merge ' 'conflicts where possible')) self.autoresolvechk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', False)) self.layout().addWidget(self.autoresolvechk) if 'hgsubversion' in repo.extensions(): self.svnchk = QCheckBox(_('Rebase unpublished onto Subversion head ' '(override source, destination)')) self.layout().addWidget(self.svnchk) else: self.svnchk = None self.cmd = cmdui.Widget(True, True, self) self.cmd.commandFinished.connect(self.commandFinished) self.showMessage.connect(self.cmd.stbar.showMessage) self.cmd.stbar.linkActivated.connect(self.linkActivated) self.layout().addWidget(self.cmd, 2) bbox = QDialogButtonBox() self.cancelbtn = bbox.addButton(QDialogButtonBox.Cancel) self.cancelbtn.clicked.connect(self.reject) self.rebasebtn = bbox.addButton(_('Rebase'), QDialogButtonBox.ActionRole) self.rebasebtn.clicked.connect(self.rebase) self.abortbtn = bbox.addButton(_('Abort'), QDialogButtonBox.ActionRole) self.abortbtn.clicked.connect(self.abort) self.layout().addWidget(bbox) self.bbox = bbox self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkFinished.connect(self._onCheckFinished) if self.checkResolve() or not (s or d): for w in (srcb, destb, sep, self.keepchk, self.collapsechk, self.keepbrancheschk): w.setHidden(True) self.cmd.setShowOutput(True) else: self.showMessage.emit(_('Checking...')) self.abortbtn.setEnabled(False) self.rebasebtn.setEnabled(False) QTimer.singleShot(0, self._wctxcleaner.check) self.setMinimumWidth(480) self.setMaximumHeight(800) self.resize(0, 340) self.setWindowTitle(_('Rebase - %s') % self.repo.displayname) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot(bool) def _onCheckFinished(self, clean): if not clean: self.rebasebtn.setEnabled(False) txt = _('Before rebase, you must ' 'commit or ' 'discard changes.') else: self.rebasebtn.setEnabled(True) txt = _('You may continue the rebase') self.showMessage.emit(txt) def rebase(self): self.rebasebtn.setEnabled(False) self.cancelbtn.setShown(False) self.keepchk.setEnabled(False) self.keepbrancheschk.setEnabled(False) self.basechk.setEnabled(False) self.collapsechk.setEnabled(False) cmdline = ['rebase', '--repository', self.repo.root] cmdline += ['--config', 'ui.merge=internal:' + (self.autoresolvechk.isChecked() and 'merge' or 'fail')] if os.path.exists(self.repo.join('rebasestate')): cmdline += ['--continue'] else: if self.keepchk.isChecked(): cmdline += ['--keep'] if self.keepbrancheschk.isChecked(): cmdline += ['--keepbranches'] if self.collapsechk.isChecked(): cmdline += ['--collapse'] if self.svnchk is not None and self.svnchk.isChecked(): cmdline += ['--svn'] else: source = self.opts.get('source') dest = self.opts.get('dest') sourcearg = '--source' if self.basechk.isChecked(): sourcearg = '--base' cmdline += [sourcearg, str(source), '--dest', str(dest)] self.repo.incrementBusyCount() self.cmd.run(cmdline) def swap(self): oldsource = self.opts.get('source', '.') olddest = self.opts.get('dest', '.') self.sourcecsinfo.update(target=olddest) self.destcsinfo.update(target=oldsource) self.opts['source'] = olddest self.opts['dest'] = oldsource def abort(self): cmdline = ['rebase', '--repository', self.repo.root, '--abort'] self.repo.incrementBusyCount() self.aborted = True self.cmd.run(cmdline) def commandFinished(self, ret): self.repo.decrementBusyCount() # TODO since hg 2.6, rebase will end with ret=1 in case of "unresolved # conflicts", so we can fine-tune checkResolve() later. if self.checkResolve() is False: msg = _('Rebase is complete') if self.aborted: msg = _('Rebase aborted') elif ret == 255: msg = _('Rebase failed') self.cmd.setShowOutput(True) # contains hint self.showMessage.emit(msg) self.rebasebtn.setEnabled(True) self.rebasebtn.setText(_('Close')) self.rebasebtn.clicked.disconnect(self.rebase) self.rebasebtn.clicked.connect(self.accept) def checkResolve(self): for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': txt = _('Rebase generated merge conflicts that must ' 'be resolved') self.rebasebtn.setEnabled(False) break else: self.rebasebtn.setEnabled(True) txt = _('You may continue the rebase') self.showMessage.emit(txt) if os.path.exists(self.repo.join('rebasestate')): self.abortbtn.setEnabled(True) self.rebasebtn.setText('Continue') return True else: self.abortbtn.setEnabled(False) return False def linkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() self.checkResolve() else: self._wctxcleaner.runCleaner(cmd) def reject(self): if os.path.exists(self.repo.join('rebasestate')): main = _('Exiting with an unfinished rebase is not recommended.') text = _('Consider aborting the rebase first.') labels = ((QMessageBox.Yes, _('&Exit')), (QMessageBox.No, _('Cancel'))) if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, labels=labels, parent=self): return super(RebaseDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/update.py0000644000076400007640000004351612231647662017701 0ustar stevesteve# update.py - Update dialog for TortoiseHg # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, csinfo, qtlib, thgrepo, resolve from PyQt4.QtCore import * from PyQt4.QtGui import * class UpdateDialog(QDialog): output = pyqtSignal(QString, QString) progress = pyqtSignal(QString, object, QString, QString, object) makeLogVisible = pyqtSignal(bool) def __init__(self, repoagent, rev=None, parent=None, opts={}): super(UpdateDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid self.grid = QGridLayout() self.grid.setSpacing(6) box.addLayout(self.grid) ### target revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) self.grid.addWidget(QLabel(_('Update to:')), 0, 0) self.grid.addWidget(combo, 0, 1) # Give the combo box a minimum width that will ensure that the dialog is # large enough to fit the additional progress bar that will appear when # updating subrepositories. combo.setMinimumWidth(450) # always include integer revision try: assert not isinstance(rev, (unicode, QString)) ctx = self.repo[rev] if isinstance(ctx.rev(), int): # could be None or patch name combo.addItem(str(ctx.rev())) except error.RepoLookupError: pass for name in repo.namedbranches: combo.addItem(hglib.tounicode(name)) tags = list(self.repo.tags()) + repo._bookmarks.keys() tags.sort(reverse=True) for tag in tags: combo.addItem(hglib.tounicode(tag)) if rev is None: selecturev = hglib.tounicode(self.repo.dirstate.branch()) else: selecturev = hglib.tounicode(str(rev)) selectindex = combo.findText(selecturev) if selectindex >= 0: combo.setCurrentIndex(selectindex) else: combo.setEditText(selecturev) ### target revision info items = ('%(rev)s', ' %(branch)s', ' %(tags)s', '
    %(summary)s') style = csinfo.labelstyle(contents=items, width=350, selectable=True) factory = csinfo.factory(self.repo, style=style) self.target_info = factory() self.grid.addWidget(QLabel(_('Target:')), 1, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addWidget(self.target_info, 1, 1) ### parent revision info self.ctxs = self.repo[None].parents() if len(self.ctxs) == 2: self.p1_info = factory() self.grid.addWidget(QLabel(_('Parent 1:')), 2, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addWidget(self.p1_info, 2, 1) self.p2_info = factory() self.grid.addWidget(QLabel(_('Parent 2:')), 3, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addWidget(self.p2_info, 3, 1) else: self.p1_info = factory() self.grid.addWidget(QLabel(_('Parent:')), 2, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addWidget(self.p1_info, 2, 1) ### options self.optbox = QVBoxLayout() self.optbox.setSpacing(6) expander = qtlib.ExpanderLabel(_('Options:'), False) expander.expanded.connect(self.show_options) row = self.grid.rowCount() self.grid.addWidget(expander, row, 0, Qt.AlignLeft | Qt.AlignTop) self.grid.addLayout(self.optbox, row, 1) self.verbose_chk = QCheckBox(_('List updated files (--verbose)')) self.discard_chk = QCheckBox(_('Discard local changes, no backup ' '(-C/--clean)')) self.merge_chk = QCheckBox(_('Always merge (when possible)')) self.autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) self.showlog_chk = QCheckBox(_('Always show command log')) self.optbox.addWidget(self.verbose_chk) self.optbox.addWidget(self.discard_chk) self.optbox.addWidget(self.merge_chk) self.optbox.addWidget(self.autoresolve_chk) self.optbox.addWidget(self.showlog_chk) s = QSettings() self.discard_chk.setChecked(bool(opts.get('clean'))) #### Persisted Options self.merge_chk.setChecked( QSettings().value('update/merge', False).toBool()) self.autoresolve_chk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', False) or s.value('update/autoresolve', False).toBool()) self.showlog_chk.setChecked(s.value('update/showlog', False).toBool()) self.verbose_chk.setChecked(s.value('update/verbose', False).toBool()) ## command widget self.cmd = cmdui.Widget(True, True, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandCanceling.connect(self.command_canceling) self.cmd.output.connect(self.output) self.cmd.makeLogVisible.connect(self.makeLogVisible) self.cmd.progress.connect(self.progress) box.addWidget(self.cmd) ## bottom buttons buttons = QDialogButtonBox() self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel) self.cancel_btn.clicked.connect(self.cancel_clicked) self.close_btn = buttons.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.reject) self.close_btn.setAutoDefault(False) self.update_btn = buttons.addButton(_('&Update'), QDialogButtonBox.ActionRole) self.update_btn.clicked.connect(self.update) self.detail_btn = buttons.addButton(_('Detail'), QDialogButtonBox.ResetRole) self.detail_btn.setAutoDefault(False) self.detail_btn.setCheckable(True) self.detail_btn.toggled.connect(self.detail_toggled) box.addWidget(buttons) # signal handlers self.rev_combo.editTextChanged.connect(self.update_info) self.discard_chk.toggled.connect(self.update_info) # dialog setting self.setLayout(box) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.setWindowTitle(_('Update - %s') % self.repo.displayname) self.setWindowIcon(qtlib.geticon('hg-update')) # prepare to show self.cmd.setHidden(True) self.cancel_btn.setHidden(True) self.detail_btn.setHidden(True) self.merge_chk.setHidden(True) self.autoresolve_chk.setHidden(True) self.showlog_chk.setHidden(True) self.update_info() if not self.update_btn.isEnabled(): self.rev_combo.lineEdit().selectAll() # need to change rev # expand options if a hidden one is checked hiddenOptionsChecked = self.hiddenSettingIsChecked() self.show_options(hiddenOptionsChecked) expander.set_expanded(hiddenOptionsChecked) ### Private Methods ### @property def repo(self): return self._repoagent.rawRepo() def hiddenSettingIsChecked(self): if self.merge_chk.isChecked() or self.autoresolve_chk.isChecked() or self.showlog_chk.isChecked(): return True else: return False def saveSettings(self): QSettings().setValue('update/verbose', self.verbose_chk.isChecked()) QSettings().setValue('update/merge', self.merge_chk.isChecked()) QSettings().setValue('update/autoresolve', self.autoresolve_chk.isChecked()) QSettings().setValue('update/showlog', self.showlog_chk.isChecked()) def update_info(self, *args): self.p1_info.update(self.ctxs[0].node()) merge = len(self.ctxs) == 2 if merge: self.p2_info.update(self.ctxs[1]) new_rev = hglib.fromunicode(self.rev_combo.currentText()) if new_rev == 'null': self.target_info.setText(_('remove working directory')) self.update_btn.setEnabled(True) return try: new_ctx = self.repo[new_rev] if not merge and new_ctx.rev() == self.ctxs[0].rev() \ and not new_ctx.bookmarks(): self.target_info.setText(_('(same as parent)')) clean = self.discard_chk.isChecked() self.update_btn.setEnabled(clean) else: self.target_info.update(self.repo[new_rev]) self.update_btn.setEnabled(True) except (error.LookupError, error.RepoLookupError, error.RepoError): self.target_info.setText(_('unknown revision!')) self.update_btn.setDisabled(True) def update(self): self.saveSettings() cmdline = ['update', '--repository', self.repo.root] if self.verbose_chk.isChecked(): cmdline += ['--verbose'] cmdline += ['--config', 'ui.merge=internal:' + (self.autoresolve_chk.isChecked() and 'merge' or 'fail')] rev = hglib.fromunicode(self.rev_combo.currentText()) activatebookmarkmode = self.repo.ui.config( 'tortoisehg', 'activatebookmarks', 'prompt') if activatebookmarkmode != 'never': bookmarks = self.repo[rev].bookmarks() if bookmarks and rev not in bookmarks: # The revision that we are updating into has bookmarks, # but the user did not refer to the revision by one of them # (probably used a revision number or hash) # Ask the user if it wants to update to one of these bookmarks # instead selectedbookmark = None if len(bookmarks) == 1: if activatebookmarkmode == 'auto': activatebookmark = True else: activatebookmark = qtlib.QuestionMsgBox( _('Activate bookmark?'), _('The selected revision (%s) has a bookmark on it ' 'called "%s".

    Do you want to activate it?' '
    You can disable this prompt by configuring ' 'Settings/Workbench/Activate Bookmarks') \ % (hglib.tounicode(rev), bookmarks[0])) if activatebookmark: selectedbookmark = bookmarks[0] else: # Even in auto mode, when there is more than one bookmark # we must ask the user which one must be activated selectedbookmark = qtlib.ChoicePrompt( _('Activate bookmark?'), _('The selected revision (%s) has %d ' 'bookmarks on it.

    Select the bookmark that you want ' 'to activate and click OK.

    Click Cancel ' 'if you don\'t want to activate any of them.

    ' '

    You can disable this prompt by configuring ' 'Settings/Workbench/Activate Bookmarks

    ') \ % (hglib.tounicode(rev), len(bookmarks)), self, bookmarks, self.repo._bookmarkcurrent).run() if selectedbookmark: rev = selectedbookmark elif self.repo[rev] == self.repo[self.repo._bookmarkcurrent]: deactivatebookmark = qtlib.QuestionMsgBox( _('Deactivate current bookmark?'), _('Do you really want to deactivate the %s ' 'bookmark?') % self.repo._bookmarkcurrent) if deactivatebookmark: cmdline = ['bookmark', '--repository', self.repo.root] if self.verbose_chk.isChecked(): cmdline += ['--verbose'] cmdline += ['-i', self.repo._bookmarkcurrent] self.repo.incrementBusyCount() self.cmd.run(cmdline) return cmdline.append('--rev') cmdline.append(rev) if self.discard_chk.isChecked(): cmdline.append('--clean') else: cur = self.repo.hgchangectx('.') try: node = self.repo.hgchangectx(rev) except (error.LookupError, error.RepoLookupError, error.RepoError): return def isclean(): '''whether WD is changed''' try: wc = self.repo[None] if wc.modified() or wc.added() or wc.removed(): return False for s in wc.substate: if wc.sub(s).dirty(): return False except EnvironmentError: return False return True def ismergedchange(): '''whether the local changes are merged (have 2 parents)''' wc = self.repo[None] return len(wc.parents()) == 2 def islocalmerge(p1, p2, clean=None): if clean is None: clean = isclean() pa = p1.ancestor(p2) return not clean and (p1 == pa or p2 == pa) def confirmupdate(clean=None): if clean is None: clean = isclean() msg = _('Detected uncommitted local changes in working tree.\n' 'Please select to continue:\n') data = {'discard': (_('&Discard'), _('Discard - discard local changes, no backup')), 'shelve': (_('&Shelve'), _('Shelve - move local changes to a patch')), 'merge': (_('&Merge'), _('Merge - allow to merge with local changes')),} opts = ['discard'] if not ismergedchange(): opts.append('shelve') if islocalmerge(cur, node, clean): opts.append('merge') dlg = QMessageBox(QMessageBox.Question, _('Confirm Update'), '', QMessageBox.Cancel, self) buttonnames = {} for name in opts: label, desc = data[name] msg += '\n' msg += desc btn = dlg.addButton(label, QMessageBox.ActionRole) buttonnames[btn] = name dlg.setDefaultButton(QMessageBox.Cancel) dlg.setText(msg) dlg.exec_() clicked = buttonnames.get(dlg.clickedButton()) return clicked # If merge-by-default, we want to merge whenever possible, # without prompting user (similar to command-line behavior) defaultmerge = self.merge_chk.isChecked() clean = isclean() if clean: cmdline.append('--check') elif not (defaultmerge and islocalmerge(cur, node, clean)): clicked = confirmupdate(clean) if clicked == 'discard': cmdline.append('--clean') elif clicked == 'shelve': from tortoisehg.hgqt import shelve dlg = shelve.ShelveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() return elif clicked == 'merge': pass # no args else: return # start updating self.repo.incrementBusyCount() self.cmd.run(cmdline) ### Signal Handlers ### def cancel_clicked(self): self.cmd.cancel() self.reject() def detail_toggled(self, checked): self.cmd.setShowOutput(checked) def show_options(self, visible): self.merge_chk.setShown(visible) self.autoresolve_chk.setShown(visible) self.showlog_chk.setShown(visible) def command_started(self): self.cmd.setShown(True) if self.showlog_chk.isChecked(): self.detail_btn.setChecked(True) self.update_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) def command_finished(self, ret): self.repo.decrementBusyCount() if ret not in (0, 1) or self.cmd.outputShown(): self.detail_btn.setChecked(True) self.close_btn.setShown(True) self.close_btn.setAutoDefault(True) self.close_btn.setFocus() self.cancel_btn.setHidden(True) else: self.accept() def accept(self): for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': qtlib.InfoMsgBox(_('Merge caused file conflicts'), _('File conflicts need to be resolved')) dlg = resolve.ResolveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() break super(UpdateDialog, self).accept() def command_canceling(self): self.cancel_btn.setDisabled(True) tortoisehg-2.10/tortoisehg/hgqt/decorators.py0000664000076400007640000000075212100577421020547 0ustar stevesteve""" Some useful decorator functions """ import time def timeit(func): """Decorator used to time the execution of a function""" def timefunc(*args, **kwargs): """wrapper""" t_1 = time.time() t_2 = time.clock() res = func(*args, **kwargs) t_3 = time.clock() t_4 = time.time() print "%s: %.2fms (time) %.2fms (clock)" % \ (func.func_name, 1000*(t_3 - t_2), 1000*(t_4 - t_1)) return res return timefunc tortoisehg-2.10/tortoisehg/hgqt/settings.py0000644000076400007640000021230112235634453020243 0ustar stevesteve# settings.py - Configuration dialog for TortoiseHg and Mercurial # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import ui, util, error, extensions, scmutil, phases from tortoisehg.util import hglib, paths, wconfig, i18n, editor from tortoisehg.util import terminal, gpg from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, qscilib, thgrepo, customtools from PyQt4.QtCore import * from PyQt4.QtGui import * if os.name == 'nt': from tortoisehg.util import bugtraq _hasbugtraq = True else: _hasbugtraq = False # Technical Debt # stacked widget or pages need to be scrollable # we need a consistent icon set # connect to thgrepo.configChanged signal and refresh _unspecstr = _('') ENTRY_WIDTH = 300 def hasExtension(extname): for name, module in extensions.extensions(): if name == extname: return True return False class SettingsCombo(QComboBox): def __init__(self, parent=None, **opts): QComboBox.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.setEditable(opts.get('canedit', False)) self.setValidator(opts.get('validator', None)) self.defaults = opts.get('defaults', []) if self.defaults and self.isEditable(): self.setCompleter(QCompleter(self.defaults, self)) self.curvalue = None self.loaded = False if 'nohist' in opts: self.previous = [] else: settings = opts['settings'] slist = settings.value('settings/'+opts['cpath']).toStringList() self.previous = [unicode(s) for s in slist if s] self.setMinimumWidth(ENTRY_WIDTH) def resetList(self): self.clear() ucur = hglib.tounicode(self.curvalue) if self.opts.get('defer') and not self.loaded: if self.curvalue == None: # unspecified self.addItem(_unspecstr) else: self.addItem(ucur or '...') return self.addItem(_unspecstr) curindex = None for s in self.defaults: if ucur == s: curindex = self.count() self.addItem(s) if self.defaults and self.previous: self.insertSeparator(len(self.defaults)+1) for m in self.previous: if ucur == m and not curindex: curindex = self.count() self.addItem(m) if curindex is not None: self.setCurrentIndex(curindex) elif self.curvalue is None: self.setCurrentIndex(0) elif self.curvalue: self.addItem(ucur) self.setCurrentIndex(self.count()-1) else: # empty string self.setEditText(ucur) def showPopup(self): if self.opts.get('defer') and not self.loaded: self.defaults = self.opts['defer']() self.loaded = True self.resetList() QComboBox.showPopup(self) ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue self.resetList() def value(self): utext = unicode(self.currentText()) if utext == _unspecstr: return None if ('nohist' in self.opts or utext in self.defaults + self.previous or not utext): return hglib.fromunicode(utext) self.previous.insert(0, utext) self.previous = self.previous[:10] settings = QSettings() settings.setValue('settings/'+self.opts['cpath'], self.previous) return hglib.fromunicode(utext) def isDirty(self): return self.value() != self.curvalue class BoolRBGroup(QWidget): def __init__(self, parent=None, **opts): QWidget.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.trueRB = QRadioButton(_('&True')) self.falseRB = QRadioButton(_('&False')) self.unspecRB = QRadioButton(_('&Unspecified')) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.trueRB) layout.addWidget(self.falseRB) layout.addWidget(self.unspecRB) self.setLayout(layout) ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue if curvalue == 'True': self.trueRB.setChecked(True) elif curvalue == 'False': self.falseRB.setChecked(True) else: self.unspecRB.setChecked(True) def value(self): if self.trueRB.isChecked(): return 'True' elif self.falseRB.isChecked(): return 'False' else: return None def isDirty(self): return self.value() != self.curvalue class LineEditBox(QLineEdit): def __init__(self, parent=None, **opts): QLineEdit.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.setMinimumWidth(ENTRY_WIDTH) ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue if curvalue: self.setText(hglib.tounicode(curvalue)) else: self.setText('') def value(self): utext = self.text() return utext and hglib.fromunicode(utext) or None def isDirty(self): return self.value() != self.curvalue class PasswordEntry(LineEditBox): def __init__(self, parent=None, **opts): QLineEdit.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.setEchoMode(QLineEdit.Password) self.setMinimumWidth(ENTRY_WIDTH) class TextEntry(QTextEdit): def __init__(self, parent=None, **opts): QTextEdit.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.setMinimumWidth(ENTRY_WIDTH) ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue if curvalue: self.setPlainText(hglib.tounicode(curvalue)) else: self.setPlainText('') def value(self): # It is not possible to set a multi-line value with an empty line utext = self.removeEmptyLines(self.toPlainText()) return utext and hglib.fromunicode(utext) or None def isDirty(self): return self.value() != self.curvalue def removeEmptyLines(self, text): if not text: return text rawlines = hglib.fromunicode(text).splitlines() lines = [] for line in rawlines: if not line.strip(): continue lines.append(line) return os.linesep.join(lines) def _describeFont(font): if not font: return _unspecstr s = unicode(font.family()) s += ", " + _("%dpt") % font.pointSize() if font.bold(): s += ", " + _("Bold") if font.italic(): s += ", " + _("Italic") if font.strikeOut(): s += ", " + _("Strike") if font.underline(): s += ", " + _("Underline") return s class FontEntry(QWidget): def __init__(self, parent=None, **opts): QWidget.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.font = None self.label = QLabel() self.setButton = QPushButton(_('&Set...')) self.clearButton = QPushButton(_('&Clear')) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.label) layout.addStretch() layout.addWidget(self.setButton) layout.addWidget(self.clearButton) self.setLayout(layout) self.setButton.clicked.connect(self.onSetClicked) self.clearButton.clicked.connect(self.onClearClicked) cpath = self.opts['cpath'] assert cpath.startswith('tortoisehg.') self.fname = cpath[11:] self.setMinimumWidth(ENTRY_WIDTH) def onSetClicked(self): thgf = qtlib.getfont(self.fname) origfont = self.font or thgf.font() font, isok = QFontDialog.getFont(origfont, self) if not isok: return self.setCurrentFont(font) thgf.setFont(font) def onClearClicked(self): self.setCurrentFont(None) def setCurrentFont(self, font): self.font = font self.label.setText(_describeFont(self.font)) ## common APIs for all edit widgets def setValue(self, curvalue): if curvalue: self.curvalue = QFont() self.curvalue.fromString(hglib.tounicode(curvalue)) else: self.curvalue = None self.setCurrentFont(self.curvalue) def value(self): if not self.font: return None utext = self.font.toString() return hglib.fromunicode(utext) def isDirty(self): return self.font != self.curvalue class SettingsCheckBox(QCheckBox): def __init__(self, parent=None, **opts): QCheckBox.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.setText(opts['label']) def setValue(self, curvalue): if self.curvalue == None: self.curvalue = curvalue self.setChecked(curvalue) def value(self): return self.isChecked() def isDirty(self): return self.value() != self.curvalue # When redesigning the structure of SettingsForm, consider to replace Spacer # by QGroupBox. class Spacer(QWidget): """Dummy widget for group separator""" def __init__(self, parent=None, **opts): super(Spacer, self).__init__(parent) if opts.get('cpath'): raise ValueError('do not set cpath for spacer') self.opts = opts def setValue(self, curvalue): raise NotImplementedError def value(self): raise NotImplementedError def isDirty(self): return False class BugTraqConfigureEntry(QPushButton): def __init__(self, parent=None, **opts): QPushButton.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.curvalue = None self.options = None self.tracker = None self.master = None self.setText(opts['label']) self.clicked.connect(self.on_clicked) def on_clicked(self, checked): parameters = self.options self.options = self.tracker.show_options_dialog(parameters) def master_updated(self): self.setEnabled(False) if self.master == None: return if self.master.value() == None: return if len(self.master.value()) == 0: return try: setting = self.master.value().split(' ', 1) trackerid = setting[0] name = setting[1] self.tracker = bugtraq.BugTraq(trackerid) except: # failed to load bugtraq module or parse the setting: # swallow the error and leave the widget disabled return try: self.setEnabled(self.tracker.has_options()) except Exception, e: qtlib.ErrorMsgBox(_('Issue Tracker'), _('Failed to load issue tracker: \'%s\': %s. ' % (name, e)), parent=self) ## common APIs for all edit widgets def setValue(self, curvalue): if self.master == None: self.master = self.opts['master'] self.master.currentIndexChanged.connect(self.master_updated) self.master_updated() self.curvalue = curvalue self.options = curvalue def value(self): return self.options def isDirty(self): return self.value() != self.curvalue class PathBrowser(QWidget): def __init__(self, parent=None, **opts): QWidget.__init__(self, parent, toolTip=opts['tooltip']) self.opts = opts self.lineEdit = QLineEdit() completer = QCompleter(self) completer.setModel(QDirModel(completer)) self.lineEdit.setCompleter(completer) self.browseButton = QPushButton(_('&Browse...')) self.browseButton.clicked.connect(self.browse) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.lineEdit) layout.addWidget(self.browseButton) self.setLayout(layout) def browse(self): dir = QFileDialog.getExistingDirectory(self, '', self.lineEdit.text()) if dir: self.lineEdit.setText(dir) ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue if curvalue: self.lineEdit.setText(hglib.tounicode(curvalue)) else: self.lineEdit.setText('') def value(self): utext = self.lineEdit.text() return utext and hglib.fromunicode(utext) or None def isDirty(self): return self.value() != self.curvalue def genEditCombo(opts, defaults=[]): opts['canedit'] = True opts['defaults'] = defaults return SettingsCombo(**opts) def genIntEditCombo(opts): 'EditCombo, only allows integer values' opts['canedit'] = True opts['validator'] = QIntValidator(None) # missing parent=None on PyQt4.6 return SettingsCombo(**opts) def genLineEditBox(opts): 'Generate a single line text entry box' return LineEditBox(**opts) def genPasswordEntry(opts): 'Generate a password entry box' return PasswordEntry(**opts) def genTextEntry(opts): 'Generate a multi-line text input entry box' return TextEntry(**opts) def genDefaultCombo(opts, defaults=[]): 'user must select from a list' opts['defaults'] = defaults opts['nohist'] = True return SettingsCombo(**opts) def genBoolRBGroup(opts): 'true, false, unspecified' return BoolRBGroup(**opts) def genDeferredCombo(opts, func): 'Values retrieved from a function at popup time' opts['defer'] = func opts['nohist'] = True return SettingsCombo(**opts) def genEditableDeferredCombo(opts, func): 'Values retrieved from a function at popup time' opts['canedit'] = True return genDeferredCombo(opts, func) def genFontEdit(opts): return FontEntry(**opts) def genSpacer(opts): return Spacer(**opts) def genBugTraqEdit(opts): return BugTraqConfigureEntry(**opts) def genPathBrowser(opts): return PathBrowser(**opts) def findIssueTrackerPlugins(): plugins = bugtraq.get_issue_plugins_with_names() names = [("%s %s" % (key[0], key[1])) for key in plugins] return names def issuePluginVisible(): if not _hasbugtraq: return False try: # quick test to see if we're able to load the bugtraq module test = bugtraq.BugTraq('') return True except: return False def findDiffTools(): return hglib.difftools(ui.ui()) def findMergeTools(): return hglib.mergetools(ui.ui()) def findEditors(): return editor.findeditors(ui.ui()) def findTerminals(): return terminal.findterminals(ui.ui()) def findGpg(): return gpg.findgpg(ui.ui()) def genCheckBox(opts): opts['nohist'] = True return SettingsCheckBox(**opts) class _fi(object): """Information of each field""" __slots__ = ('label', 'cpath', 'values', 'tooltip', 'restartneeded', 'globalonly', 'master', 'visible') def __init__(self, label, cpath, values, tooltip, restartneeded=False, globalonly=False, master=None, visible=None): self.label = label self.cpath = cpath self.values = values self.tooltip = tooltip self.restartneeded = restartneeded self.globalonly = globalonly self.master = master self.visible = visible def isVisible(self): if self.visible == None: return True else: return self.visible() INFO = ( ({'name': 'general', 'label': 'TortoiseHg', 'icon': 'thg_logo'}, ( _fi(_('UI Language'), 'tortoisehg.ui.language', (genDeferredCombo, i18n.availablelanguages), _('Specify your preferred user interface language (restart needed)'), restartneeded=True, globalonly=True), _fi(_('Three-way Merge Tool'), 'ui.merge', (genDeferredCombo, findMergeTools), _('Graphical merge program for resolving merge conflicts. If left ' 'unspecified, Mercurial will use the first applicable tool it finds ' 'on your system or use its internal merge tool that leaves conflict ' 'markers in place. Choose internal:merge to force conflict markers, ' 'internal:prompt to always select local or other, or internal:dump ' 'to leave files in the working directory for manual merging')), _fi(_('Visual Diff Tool'), 'tortoisehg.vdiff', (genDeferredCombo, findDiffTools), _('Specify visual diff tool, as described in the [merge-tools] ' 'section of your Mercurial configuration files. If left ' 'unspecified, TortoiseHg will use the selected merge tool. ' 'Failing that it uses the first applicable tool it finds.')), _fi(_('Visual Editor'), 'tortoisehg.editor', (genEditableDeferredCombo, findEditors), _('Specify visual editor, as described in the [editor-tools] ' 'section of your Mercurial configuration files. If left ' 'unspecified, TortoiseHg will use the first applicable tool ' 'it finds.')), _fi(_('Shell'), 'tortoisehg.shell', (genEditableDeferredCombo, findTerminals), _('Specify the command to launch your preferred terminal shell ' 'application. If the value includes the string %(reponame)s, the ' 'name of the repository will be substituted in place of ' '%(reponame)s. (restart needed)
    ' 'Default, Windows: cmd.exe /K title %(reponame)s
    ' 'Default, OS X: not set
    ' 'Default, other: xterm -T "%(reponame)s"'), globalonly=True), _fi(_('Immediate Operations'), 'tortoisehg.immediate', genEditCombo, _('Space separated list of shell operations you would like ' 'to be performed immediately, without user interaction. ' 'Commands are "add remove revert forget". ' 'Default: None (leave blank)')), _fi(_('Tab Width'), 'tortoisehg.tabwidth', genIntEditCombo, _('Specify the number of spaces that tabs expand to in various ' 'TortoiseHg windows. ' 'Default: 8')), _fi(_('Force Repo Tab'), 'tortoisehg.forcerepotab', genBoolRBGroup, _('Always show repo tabs, even for a single repo. Default: False')), _fi(_('Monitor Repo Changes'), 'tortoisehg.monitorrepo', (genDefaultCombo, ['always', 'localonly', 'never']), _('Specify the target filesystem where TortoiseHg monitors changes. ' 'Default: always')), _fi(_('Max Diff Size'), 'tortoisehg.maxdiff', genIntEditCombo, _('The maximum size file (in KB) that TortoiseHg will ' 'show changes for in the changelog, status, and commit windows. ' 'A value of zero implies no limit. Default: 1024 (1MB)')), _fi(_('Fork GUI'), 'tortoisehg.guifork', genBoolRBGroup, _('When running from the command line, fork a background ' 'process to run graphical dialogs. Default: True')), _fi(_('Full Path Title'), 'tortoisehg.fullpath', genBoolRBGroup, _('Show a full directory path of the repository in the dialog title ' 'instead of just the root directory name. Default: False')), _fi(_('Auto-resolve merges'), 'tortoisehg.autoresolve', genBoolRBGroup, _('Indicates whether TortoiseHg should attempt to automatically ' 'resolve changes from both sides to the same file, and only report ' 'merge conflicts when this is not possible. When False, all files ' 'with changes on both sides of the merge will report as conflicting, ' 'even if the edits are to different parts of the file. In either ' 'case, when conflicts occur, the user will be invited to review and ' 'resolve changes manually. Default: False.')), )), ({'name': 'log', 'label': _('Workbench'), 'icon': 'menulog'}, ( _fi(_('Single Workbench Window'), 'tortoisehg.workbench.single', genBoolRBGroup, _('Select whether you want to have a single workbench window. ' 'If you disable this setting you will get a new workbench window ' 'everytime that you use the "Hg Workbench" command on the explorer ' 'context menu. Default: True'), restartneeded=True, globalonly=True), _fi(_('Default widget'), 'tortoisehg.defaultwidget', (genDefaultCombo, ['revdetails', 'commit', 'sync', 'manifest', 'search']), _('Select the initial widget that will be shown when opening a ' 'repository. ' 'Default: revdetails')), _fi(_('Initial revision'), 'tortoisehg.initialrevision', (genDefaultCombo, ['current', 'tip', 'workingdir']), _('Select the initial revision that will be selected when opening a ' 'repository. You can select the "current" (i.e. the working ' 'directory parent), the current "tip" or the working directory ' '("workingdir"). ' 'Default: current')), _fi(_('Open new tabs next\nto the current tab'), 'tortoisehg.opentabsaftercurrent', genBoolRBGroup, _('Should new tabs be open next to the current tab? ' 'If False new tabs will be open after the last tab. ' 'Default: True')), _fi(_('Author Coloring'), 'tortoisehg.authorcolor', genBoolRBGroup, _('Color changesets by author name. If not enabled, ' 'the changes are colored green for merge, red for ' 'non-trivial parents, black for normal. ' 'Default: False')), _fi(_('Full Authorname'), 'tortoisehg.fullauthorname', genBoolRBGroup, _('Show full authorname in Logview. If not enabled, ' 'only a short part, usually name without email is shown. ' 'Default: False')), _fi(_('Task Tabs'), 'tortoisehg.tasktabs', (genDefaultCombo, ['east', 'west', 'off']), _('Show tabs along the side of the bottom half of each repo ' 'widget allowing one to switch task tabs without using the toolbar. ' 'Default: off')), _fi(_('Task Toolbar Order'), 'tortoisehg.workbench.task-toolbar', genEditCombo, _('Specify which task buttons you want to show on the task toolbar ' 'and in which order.
    Type a list of the task button names. ' 'Add separators by putting "|" between task button names.
    ' 'Valid names are: log commit mq sync manifest grep and pbranch.
    ' 'Default: log commit manifest grep pbranch | sync'), restartneeded=True, globalonly=True), _fi(_('Long Summary'), 'tortoisehg.longsummary', genBoolRBGroup, _('If true, concatenate multiple lines of changeset summary ' 'until they reach 80 characters. ' 'Default: False')), _fi(_('Log Batch Size'), 'tortoisehg.graphlimit', genIntEditCombo, _('The number of revisions to read and display in the ' 'changelog viewer in a single batch. ' 'Default: 500')), _fi(_('Dead Branches'), 'tortoisehg.deadbranch', genEditCombo, _('Comma separated list of branch names that should be ignored ' 'when building a list of branch names for a repository. ' 'Default: None (leave blank)')), _fi(_('Branch Colors'), 'tortoisehg.branchcolors', genEditCombo, _('Space separated list of branch names and colors of the form ' 'branch:#XXXXXX. Spaces and colons in the branch name must be ' 'escaped using a backslash (\\). Likewise some other characters ' 'can be escaped in this way, e.g. \\u0040 will be decoded to the ' '@ character, and \\n to a linefeed. ' 'Default: None (leave blank)')), _fi(_('Hide Tags'), 'tortoisehg.hidetags', genEditCombo, _('Space separated list of tags that will not be shown.' 'Useful example: Specify "qbase qparent qtip" to hide the ' 'standard tags inserted by the Mercurial Queues Extension. ' 'Default: None (leave blank)')), _fi(_('Activate Bookmarks'), 'tortoisehg.activatebookmarks', (genDefaultCombo, ['auto', 'prompt', 'never']), _('Select when TortoiseHg will show a prompt to activate a bookmark ' 'when updating to a revision that has one or more bookmarks.' '

    • auto: Try to automatically activate bookmarks. When ' 'updating to a revision that has a single bookmark it will be ' 'activated automatically. Show a prompt if there is more than one ' 'bookmark on the revision that is being updated to.' '
    • prompt: The default. Show a prompt when updating to a ' 'revision that has one or more bookmarks.' '
    • never: Never show any prompt to activate any bookmarks.' '

    ' 'Default: prompt')), )), ({'name': 'commit', 'label': _('Commit', 'config item'), 'icon': 'menucommit'}, ( _fi(_('Username'), 'ui.username', genEditCombo, _('Name associated with commits. The common format is:
    ' 'Full Name <email@example.com>')), _fi(_('Summary Line Length'), 'tortoisehg.summarylen', genIntEditCombo, _('Suggested length of commit message lines. A red vertical ' 'line will mark this length. CTRL-E will reflow the current ' 'paragraph to the specified line length. Default: 80')), _fi(_('Close After Commit'), 'tortoisehg.closeci', genBoolRBGroup, _('Close the commit tool after every successful ' 'commit. Default: False')), _fi(_('Push After Commit'), 'tortoisehg.cipushafter', (genEditCombo, ['default-push', 'default']), _('Attempt to push to specified URL or alias after each successful ' 'commit. Default: No push')), _fi(_('Auto Commit List'), 'tortoisehg.autoinc', genEditCombo, _('Comma separated list of files that are automatically included ' 'in every commit. Intended for use only as a repository setting. ' 'Default: None (leave blank)')), _fi(_('Auto Exclude List'), 'tortoisehg.ciexclude', genEditCombo, _('Comma separated list of files that are automatically unchecked ' 'when the status, and commit dialogs are opened. ' 'Default: None (leave blank)')), _fi(_('English Messages'), 'tortoisehg.engmsg', genBoolRBGroup, _('Generate English commit messages even if LANGUAGE or LANG ' 'environment variables are set to a non-English language. ' 'This setting is used by the Merge, Tag and Backout dialogs. ' 'Default: False')), _fi(_('New Commit Phase'), 'phases.new-commit', (genDefaultCombo, phases.phasenames), _('The phase of new commits. Default: draft')), _fi(_('Secret MQ Patches'), 'mq.secret', genBoolRBGroup, _('Make MQ patches secret (instead of draft). ' 'Default: False')), _fi(_('Monitor working
    directory changes'), 'tortoisehg.refreshwdstatus', (genDefaultCombo, ['auto', 'always', 'alwayslocal']), _('Select when the working directory status list will be refreshed:
    ' '- auto: [default] let TortoiseHg decide when to ' 'refresh the working directory status list.
    ' 'TortoiseHg will refresh the status list whenever it performs an ' 'action that may potentially modify the working directory. ' "This may miss any changes that happen outside of TortoiseHg's " 'control;
    ' '- always: in addition to the automatic updates above, also ' 'refresh the status list whenever the user clicks on the "working ' 'dir revision" or on the "Commit icon" on the workbench task bar;
    ' '- alwayslocal: same as "always" but restricts forced ' 'refreshes to local repos.
    ' 'Default: auto')), _fi(_('Confirm adding unknown files'), 'tortoisehg.confirmaddfiles', genBoolRBGroup, _('Determines if TortoiseHg should show a confirmation dialog ' 'before adding new files in a commit. ' 'If True, a confirmation dialog will be showed. ' 'If False, selected new files will be included in the ' 'commit with no confirmation dialog. Default: True')), _fi(_('Confirm deleting files'), 'tortoisehg.confirmdeletefiles', genBoolRBGroup, _('Determines if TortoiseHg should show a confirmation dialog ' 'before removing files in a commit. ' 'If True, a confirmation dialog will be showed. ' 'If False, selected deleted files will be included in the ' 'commit with no confirmation dialog. Default: True')), )), ({'name': 'sync', 'label': _('Sync'), 'icon': 'thg-sync'}, ( _fi(_('After Pull Operation'), 'tortoisehg.postpull', (genDefaultCombo, ['none', 'update', 'fetch', 'rebase', 'updateorrebase']), _('Operation which is performed directly after a successful pull. ' 'update equates to pull --update, fetch equates to the fetch ' 'extension, rebase equates to pull --rebase, ' 'updateorrebase equates to pull -u --rebase. Default: none')), _fi(_('Default Push'), 'tortoisehg.defaultpush', (genDefaultCombo, ['all', 'branch', 'revision']), _('Select the revisions that will be pushed by default, ' 'whenever you click the Push button.' '

    • all: The default. Push all changes in ' 'all branches.' '
    • branch: Push all changes in the current branch.' '
    • revision: Push the changes in the current branch ' 'up to the current revision.

    ' 'Default: all')), _fi(_('Confirm Push'), 'tortoisehg.confirmpush', genBoolRBGroup, _('Determines if TortoiseHg should show a confirmation dialog ' 'before pushing changesets. ' 'If False, push will be performed without any confirmation dialog. ' 'Default: True')), _fi(_('Target Combo'), 'tortoisehg.workbench.target-combo', (genDefaultCombo, ['auto', 'always']), _('Select if TortoiseHg will show a target combo in the sync toolbar.' '

    • auto: The default. Show the combo if more than one ' 'target configured.' '
    • always: Always show the combo.' '

    ' 'Default: auto')), _fi(_('SSH Command'), 'ui.ssh', genEditCombo, _('Command to use for SSH connections.

    ' 'Default: "ssh" or "TortoisePlink.exe -ssh -2" (Windows)')), )), ({'name': 'web', 'label': _('Server'), 'icon': 'proxy'}, ( _fi(_('Behavior:'), None, genSpacer, ''), _fi(_("'Publishing' repository"), 'phases.publish', genBoolRBGroup, _('Controls draft phase behavior when working as a server. When true, ' 'pushed changesets are set to public in both client and server and ' 'pulled or cloned changesets are set to public in the client. ' 'Default: True')), _fi(_('Web Server:'), None, genSpacer, ''), _fi(_('Name'), 'web.name', genEditCombo, _('Repository name to use in the web interface, and by TortoiseHg ' 'as a shorthand name. Default is the working directory.')), _fi(_('Description'), 'web.description', genEditCombo, _("Textual description of the repository's purpose or " 'contents.')), _fi(_('Contact'), 'web.contact', genEditCombo, _('Name or email address of the person in charge of the ' 'repository.')), _fi(_('Style'), 'web.style', (genDefaultCombo, ['paper', 'monoblue', 'coal', 'spartan', 'gitweb', 'old']), _('Which template map style to use')), _fi(_('Archive Formats'), 'web.allow_archive', (genEditCombo, ['bz2', 'gz', 'zip']), _('Comma separated list of archive formats allowed for ' 'downloading')), _fi(_('Port'), 'web.port', genIntEditCombo, _('Port to listen on')), _fi(_('Push Requires SSL'), 'web.push_ssl', genBoolRBGroup, _('Whether to require that inbound pushes be transported ' 'over SSL to prevent password sniffing.')), _fi(_('Stripes'), 'web.stripes', genIntEditCombo, _('How many lines a "zebra stripe" should span in multiline output. ' 'Default is 1; set to 0 to disable.')), _fi(_('Max Files'), 'web.maxfiles', genIntEditCombo, _('Maximum number of files to list per changeset. Default: 10')), _fi(_('Max Changes'), 'web.maxchanges', genIntEditCombo, _('Maximum number of changes to list on the changelog. ' 'Default: 10')), _fi(_('Allow Push'), 'web.allow_push', (genEditCombo, ['*']), _('Whether to allow pushing to the repository. If empty or not ' 'set, push is not allowed. If the special value "*", any remote ' 'user can push, including unauthenticated users. Otherwise, the ' 'remote user must have been authenticated, and the authenticated ' 'user name must be present in this list (separated by whitespace ' 'or ","). The contents of the allow_push list are examined after ' 'the deny_push list.')), _fi(_('Deny Push'), 'web.deny_push', (genEditCombo, ['*']), _('Whether to deny pushing to the repository. If empty or not set, ' 'push is not denied. If the special value "*", all remote users ' 'are denied push. Otherwise, unauthenticated users are all ' 'denied, and any authenticated user name present in this list ' '(separated by whitespace or ",") is also denied. The contents ' 'of the deny_push list are examined before the allow_push list.')), _fi(_('Encoding'), 'web.encoding', (genEditCombo, ['UTF-8']), _('Character encoding name')), )), ({'name': 'proxy', 'label': _('Proxy'), 'icon': QStyle.SP_DriveNetIcon}, ( _fi(_('Host'), 'http_proxy.host', genEditCombo, _('Host name and (optional) port of proxy server, for ' 'example "myproxy:8000"')), _fi(_('Bypass List'), 'http_proxy.no', genEditCombo, _('Optional. Comma-separated list of host names that ' 'should bypass the proxy')), _fi(_('User'), 'http_proxy.user', genEditCombo, _('Optional. User name to authenticate with at the proxy server')), _fi(_('Password'), 'http_proxy.passwd', genPasswordEntry, _('Optional. Password to authenticate with at the proxy server')), )), ({'name': 'email', 'label': _('Email'), 'icon': 'mail-forward'}, ( _fi(_('From'), 'email.from', genEditCombo, _('Email address to use in the "From" header and for ' 'the SMTP envelope')), _fi(_('To'), 'email.to', genEditCombo, _('Comma-separated list of recipient email addresses')), _fi(_('Cc'), 'email.cc', genEditCombo, _('Comma-separated list of carbon copy recipient email addresses')), _fi(_('Bcc'), 'email.bcc', genEditCombo, _('Comma-separated list of blind carbon copy recipient ' 'email addresses')), _fi(_('method'), 'email.method', (genEditCombo, ['smtp']), _('Optional. Method to use to send email messages. If value is ' '"smtp" (default), use SMTP (configured below). Otherwise, use as ' 'name of program to run that acts like sendmail (takes "-f" option ' 'for sender, list of recipients on command line, message on stdin). ' 'Normally, setting this to "sendmail" or "/usr/sbin/sendmail" ' 'is enough to use sendmail to send messages.')), _fi(_('SMTP Host'), 'smtp.host', genEditCombo, _('Host name of mail server')), _fi(_('SMTP Port'), 'smtp.port', genIntEditCombo, _('Port to connect to on mail server. ' 'Default: 25')), _fi(_('SMTP TLS'), 'smtp.tls', genBoolRBGroup, _('Connect to mail server using TLS. ' 'Default: False')), _fi(_('SMTP Username'), 'smtp.username', genEditCombo, _('Username to authenticate to mail server with')), _fi(_('SMTP Password'), 'smtp.password', genPasswordEntry, _('Password to authenticate to mail server with')), _fi(_('Local Hostname'), 'smtp.local_hostname', genEditCombo, _('Hostname the sender can use to identify itself to the ' 'mail server.')), )), ({'name': 'diff', 'label': _('Diff and Annotate'), 'icon': QStyle.SP_FileDialogContentsView}, ( _fi(_('Patch EOL'), 'patch.eol', (genDefaultCombo, ['auto', 'strict', 'crlf', 'lf']), _('Normalize file line endings during and after patch to lf or ' 'crlf. Strict does no normalization. Auto does per-file ' 'detection, and is the recommended setting. ' 'Default: strict')), _fi(_('Git Format'), 'diff.git', genBoolRBGroup, _('Use git extended diff header format. ' 'Default: False')), _fi(_('MQ Git Format'), 'mq.git', (genDefaultCombo, ['auto', 'keep', 'yes', 'no']), _("When set to 'auto', mq will automatically use git patches when " "required to avoid losing changes to file modes, copy records or " "binary files. If set to 'keep', mq will obey the [diff] section " "configuration while preserving existing git patches upon qrefresh. " "If set to 'yes' or 'no', mq will override the [diff] section and " "always generate git or regular patches, possibly losing data in the " "second case. " "Default: auto")), _fi(_('No Dates'), 'diff.nodates', genBoolRBGroup, _('Do not include modification dates in diff headers. ' 'Default: False')), _fi(_('Show Function'), 'diff.showfunc', genBoolRBGroup, _('Show which function each change is in. ' 'Default: False')), _fi(_('Ignore White Space'), 'diff.ignorews', genBoolRBGroup, _('Ignore white space when comparing lines in diff views. ' 'Default: False')), _fi(_('Ignore WS Amount'), 'diff.ignorewsamount', genBoolRBGroup, _('Ignore changes in the amount of white space in diff views. ' 'Default: False')), _fi(_('Ignore Blank Lines'), 'diff.ignoreblanklines', genBoolRBGroup, _('Ignore changes whose lines are all blank in diff views. ' 'Default: False')), _fi(_('Annotate:'), None, genSpacer, ''), _fi(_('Ignore White Space'), 'annotate.ignorews', genBoolRBGroup, _('Ignore white space when comparing lines in the annotate view. ' 'Default: False')), _fi(_('Ignore WS Amount'), 'annotate.ignorewsamount', genBoolRBGroup, _('Ignore changes in the amount of white space in the annotate view. ' 'Default: False')), _fi(_('Ignore Blank Lines'), 'annotate.ignoreblanklines', genBoolRBGroup, _('Ignore changes whose lines are all blank in the annotate view. ' 'Default: False')), )), ({'name': 'fonts', 'label': _('Fonts'), 'icon': 'preferences-desktop-font'}, ( _fi(_('Message Font'), 'tortoisehg.fontcomment', genFontEdit, _('Font used to display commit messages. Default: monospace 10'), globalonly=True), _fi(_('Diff Font'), 'tortoisehg.fontdiff', genFontEdit, _('Font used to display text differences. Default: monospace 10'), globalonly=True), _fi(_('List Font'), 'tortoisehg.fontlist', genFontEdit, _('Font used to display file lists. Default: sans 9'), globalonly=True), _fi(_('ChangeLog Font'), 'tortoisehg.fontlog', genFontEdit, _('Font used to display changelog data. Default: monospace 10'), globalonly=True), _fi(_('Output Font'), 'tortoisehg.fontoutputlog', genFontEdit, _('Font used to display output messages. Default: sans 8'), globalonly=True), )), ({'name': 'extensions', 'label': _('Extensions'), 'icon': 'hg-extensions'}, ( )), ({'name': 'tools', 'label': _('Tools'), 'icon': 'tools-spanner-hammer'}, ( )), ({'name': 'hooks', 'label': _('Hooks'), 'icon': 'tools-hooks'}, ( )), ({'name': 'issue', 'label': _('Issue Tracking'), 'icon': 'edit-file'}, ( _fi(_('Issue Regex'), 'tortoisehg.issue.regex', genEditCombo, _('Defines the regex to match when picking up issue numbers.')), _fi(_('Issue Link'), 'tortoisehg.issue.link', genEditCombo, _('Defines the command to run when an issue number is recognized. ' 'You may include groups in issue.regex, and corresponding {n} ' 'tokens in issue.link (where n is a non-negative integer). ' '{0} refers to the entire string matched by issue.regex, ' 'while {1} refers to the first group and so on. If no {n} tokens' 'are found in issue.link, the entire matched string is appended ' 'instead.')), _fi(_('Inline Tags'), 'tortoisehg.issue.inlinetags', genBoolRBGroup, _('Show tags at start of commit message.')), _fi(_('Mandatory Issue Reference'), 'tortoisehg.issue.linkmandatory', genBoolRBGroup, _('When committing, require that a reference to an issue be specified. ' 'If enabled, the regex configured in \'Issue Regex\' must find a ' 'match in the commit message.')), _fi(_('Issue Tracker Plugin'), 'tortoisehg.issue.bugtraqplugin', (genDeferredCombo, findIssueTrackerPlugins), _('Configures a COM IBugTraqProvider or IBugTrackProvider2 issue ' 'tracking plugin.'), visible=issuePluginVisible), _fi(_('Configure Issue Tracker'), 'tortoisehg.issue.bugtraqparameters', genBugTraqEdit, _('Configure the selected COM Bug Tracker plugin.'), master='tortoisehg.issue.bugtraqplugin', visible=issuePluginVisible), _fi(_('Issue Tracker Trigger'), 'tortoisehg.issue.bugtraqtrigger', (genDefaultCombo, ['never', 'commit']), _('Determines when the issue tracker state will be updated by ' 'TortoiseHg. Valid settings values are:' '

    • never: Do not update the Issue Tracker state ' 'automatically.' '
    • commit: Update the Issue Tracker state after a ' 'successful commit.

    ' 'Default: never'), master='tortoisehg.issue.bugtraqplugin', visible=issuePluginVisible), _fi(_('Changeset Link'), 'tortoisehg.changeset.link', genEditCombo, _('A "template string" that, when set, turns the revision number and ' 'short hashes that are shown on the revision panels into links.
    ' 'The "template string" uses a "mercurial template"-like syntax that ' 'currently accepts two template expressions:' '

      ' '
    • {node|short} : replaced by the 12 digit revision id (note that ' '{node} on its own is currently unsupported).' '
    • {rev} : replaced by the revision number.' '
    ' 'For example, in order to link to bitbucket commit pages you can ' 'set this to:
    ' 'https://bitbucket.org/tortoisehg/thg/commits/{node|short}' )), )), ({'name': 'reviewboard', 'label': _('Review Board'), 'icon': 'reviewboard'}, ( _fi(_('Server'), 'reviewboard.server', genEditCombo, _('Path to review board ' 'example "http://demo.reviewboard.org"')), _fi(_('User'), 'reviewboard.user', genEditCombo, _('User name to authenticate with review board')), _fi(_('Password'), 'reviewboard.password', genPasswordEntry, _('Password to authenticate with review board')), _fi(_('Server Repository ID'), 'reviewboard.repoid', genEditCombo, _('The default repository id for this repo on the review board ' 'server')), _fi(_('Target Groups'), 'reviewboard.target_groups', genEditCombo, _('A comma separated list of target groups')), _fi(_('Target People'), 'reviewboard.target_people', genEditCombo, _('A comma separated list of target people')), )), ({'name': 'kbfiles', 'label': _('Kiln Bfiles'), 'icon': 'kiln', 'extension': 'kbfiles'}, ( _fi(_('Patterns'), 'kilnbfiles.patterns', genEditCombo, _('Files with names meeting the specified patterns will be ' 'automatically added as bfiles')), _fi(_('Size'), 'kilnbfiles.size', genEditCombo, _('Files of at least the specified size (in megabytes) will be added ' 'as bfiles')), _fi(_('System Cache'), 'kilnbfiles.systemcache', genPathBrowser, _('Path to the directory where a system-wide cache of bfiles will be ' 'stored')), )), ({'name': 'largefiles', 'label': _('Largefiles'), 'icon': 'kiln', 'extension': 'largefiles'}, ( _fi(_('Patterns'), 'largefiles.patterns', genEditCombo, _('Files with names meeting the specified patterns will be ' 'automatically added as largefiles')), _fi(_('Minimum Size'), 'largefiles.minsize', genEditCombo, _('Files of at least the specified size (in megabytes) will be added ' 'as largefiles')), _fi(_('User Cache'), 'largefiles.usercache', genPathBrowser, _('Path to the directory where a user\'s cache of largefiles will be ' 'stored')), )), ({'name': 'projrc', 'label': _('Projrc'), 'icon': 'settings_projrc', 'extension': 'projrc'}, ( _fi(_('Require confirmation'), 'projrc.confirm', (genDefaultCombo, ['always', 'first', 'never']), _('When to ask the user to confirm the update of the local "projrc" ' 'configuration file when the remote projrc file changes. Possible ' 'values are:' '
    • always: [default] ' 'Always show a confirmation prompt before updating the local ' '.hg/projrc file.' '
    • first: Show a confirmation dialog when the repository is ' 'cloned or when a remote projrc file is found for the first time.' '
    • never: Update the local .hg/projrc file automatically, ' 'without requiring any user confirmation.
    ')), _fi(_('Servers'), 'projrc.servers', genEditCombo, _('List of Servers from which "projrc" configuration files must be ' 'pulled. Set it to "*" to pull from all servers. Set it to "default" ' 'to pull from the default sync path.' 'Default is pull from NO servers.')), _fi(_('Include'), 'projrc.include', genEditCombo, _('List of settings that will be pulled from the project configuration ' 'file. Default is include NO settings.')), _fi(_('Exclude'), 'projrc.exclude', genEditCombo, _('List of settings that will NOT be pulled from the project ' 'configuration file. ' 'Default is exclude none of the included settings.')), _fi(_('Update on incoming'), 'projrc.updateonincoming', (genDefaultCombo, ['never', 'prompt', 'auto']), _('Let the user update the projrc on incoming:' '
    • never: [default] ' 'Show whether the remote projrc file has changed, ' 'but do not update (nor ask to update) the local projrc file.' '
    • prompt: Look for changes to the projrc file. ' 'If there are changes _always_ show a confirmation prompt, ' 'asking the user if it wants to update its local projrc file.' '
    • auto: Look for changes to the projrc file. ' 'Use the value of the "projrc.confirm" configuration key to ' 'determine whether to show a confirmation dialog or not ' 'before updating the local projrc file.

    ' 'Default: never')), )), ({'name': 'gnupg', 'label': _('GnuPG'), 'icon': 'gnupg', 'extension': 'gpg'}, ( _fi(_('Command'), 'gpg.cmd', (genEditableDeferredCombo, findGpg), _('Specify the path to GPG. Default: gpg')), _fi(_('Key ID'), 'gpg.key', genEditCombo, _('GPG key ID associated with user. Default: None (leave blank)')), )), ) CONF_GLOBAL = 0 CONF_REPO = 1 class SettingsDialog(QDialog): 'Dialog for editing Mercurial.ini or hgrc' def __init__(self, configrepo=False, focus=None, parent=None, root=None): QDialog.__init__(self, parent) self.setWindowTitle(_('TortoiseHg Settings')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self.setWindowIcon(qtlib.geticon('settings_repo')) if not hasattr(wconfig.config(), 'write'): qtlib.ErrorMsgBox(_('Iniparse package not found'), _("Can't change settings without iniparse package - " 'view is readonly.'), parent=self) print 'Please install http://code.google.com/p/iniparse/' if not focus: focus = QSettings().value('settings/lastpage', 'log').toString() focus = unicode(focus) layout = QVBoxLayout() self.setLayout(layout) s = QSettings() self.settings = s self.restoreGeometry(s.value('settings/geom').toByteArray()) def username(): name = util.username() if name: return hglib.tounicode(name) name = os.environ.get('USERNAME') if name: return hglib.tounicode(name) return _('User') self._activeformidx = configrepo and CONF_REPO or CONF_GLOBAL self.conftabs = QTabWidget() layout.addWidget(self.conftabs) utab = SettingsForm(rcpath=scmutil.userrcpath(), focus=focus) self.conftabs.addTab(utab, qtlib.geticon('settings_user'), _("%s's global settings") % username()) utab.restartRequested.connect(self._pushRestartRequest) try: if root is None: root = paths.find_root() if root: repo = thgrepo.repository(ui.ui(), root) else: repo = None except error.RepoError: repo = None if configrepo: uroot = hglib.tounicode(root) qtlib.ErrorMsgBox(_('No repository found'), _('no repo at ') + uroot, parent=self) if repo: if 'projrc' in repo.extensions(): projrcpath = os.sep.join([repo.root, '.hg', 'projrc']) if os.path.exists(projrcpath): rtab = SettingsForm(rcpath=projrcpath, focus=focus, readonly=True) self.conftabs.addTab(rtab, qtlib.geticon('settings_projrc'), _('%s project settings (.hg/projrc)') % repo.shortname) rtab.restartRequested.connect(self._pushRestartRequest) reporcpath = os.sep.join([repo.root, '.hg', 'hgrc']) rtab = SettingsForm(rcpath=reporcpath, focus=focus) self.conftabs.addTab(rtab, qtlib.geticon('settings_repo'), _('%s repository settings') % repo.shortname) rtab.restartRequested.connect(self._pushRestartRequest) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) layout.addWidget(bb) self.bb = bb self._restartreqs = set() self.conftabs.setCurrentIndex(self._activeformidx) self.conftabs.currentChanged.connect(self._currentFormChanged) def isDirty(self): return util.any(self.conftabs.widget(i).isDirty() for i in xrange(self.conftabs.count())) @pyqtSlot(unicode) def _pushRestartRequest(self, key): self._restartreqs.add(unicode(key)) def applyChanges(self): results = [self.conftabs.widget(i).applyChanges() for i in xrange(self.conftabs.count())] if self._restartreqs: qtlib.InfoMsgBox(_('Settings'), _('Restart all TortoiseHg applications ' 'for the following changes to take effect:'), ', '.join(sorted(self._restartreqs))) self._restartreqs.clear() return util.all(results) def canExit(self): if self.isDirty(): ret = qtlib.CustomPrompt(_('Confirm Exit'), _('Apply changes before exit?'), self, (_('&Yes'), _('&No (discard changes)'), _ ('Cancel')), default=2, esc=2).run() if ret == 2: return False elif ret == 0: return self.applyChanges() return True def accept(self): if not self.applyChanges(): return s = self.settings s.setValue('settings/geom', self.saveGeometry()) s.setValue('settings/lastpage', self._getactivepagename()) s.sync() QDialog.accept(self) def reject(self): if not self.canExit(): return s = self.settings s.setValue('settings/geom', self.saveGeometry()) s.setValue('settings/lastpage', self._getactivepagename()) s.sync() QDialog.reject(self) def _getactivepagename(self): if self._activeformidx is None: return '' activeform = self.conftabs.widget(self._activeformidx) if not activeform: return '' return activeform._activepagename def _currentFormChanged(self, idx): activepagename = self._getactivepagename() if activepagename: self.conftabs.widget(idx).focusPage(activepagename) self._activeformidx = idx class SettingsForm(QWidget): """Widget for each settings file""" restartRequested = pyqtSignal(unicode) def __init__(self, rcpath, focus=None, parent=None, readonly=False): super(SettingsForm, self).__init__(parent) # If forcereadonly is false, the settings form will be readonly # if the corresponding ini file is readonly self.forcereadonly = readonly if isinstance(rcpath, (list, tuple)): self.rcpath = rcpath else: self.rcpath = [rcpath] layout = QVBoxLayout() self.setLayout(layout) tophbox = QHBoxLayout() layout.addLayout(tophbox) self.fnedit = QLineEdit() self.fnedit.setReadOnly(True) self.fnedit.setFrame(False) self.fnedit.setFocusPolicy(Qt.NoFocus) self.fnedit.setStyleSheet('QLineEdit { background: transparent; }') edit = QPushButton(_('Edit File')) edit.clicked.connect(self.editClicked) self.editbtn = edit reload = QPushButton(_('Reload')) reload.clicked.connect(self.reloadClicked) self.reloadbtn = reload tophbox.addWidget(QLabel(_('Settings File:'))) tophbox.addWidget(self.fnedit) tophbox.addWidget(edit) tophbox.addWidget(reload) bothbox = QHBoxLayout() layout.addLayout(bothbox, 8) pageList = QListWidget() pageList.setResizeMode(QListView.Fixed) stack = QStackedWidget() bothbox.addWidget(pageList, 0) bothbox.addWidget(stack, 1) pageList.currentRowChanged.connect(self.activatePage) self.pages = {} self.stack = stack self.pageList = pageList self.pageListIndexToStack = {} desctext = QTextBrowser() desctext.setOpenExternalLinks(True) layout.addWidget(desctext, 2) self.desctext = desctext self.settings = QSettings() # add page items to treeview for meta, info in INFO: if 'extension' in meta and not hasExtension(meta['extension']): continue if isinstance(meta['icon'], str): icon = qtlib.geticon(meta['icon']) else: style = QApplication.style() icon = QIcon() icon.addPixmap(style.standardPixmap(meta['icon'])) item = QListWidgetItem(icon, meta['label']) pageList.addItem(item) self.refresh() if not self.focusField(focus): # The selected setting may not exist # (e.g. if an extension has been disabled) self.pageList.setCurrentRow(0) @pyqtSlot(int) def activatePage(self, index): if index >= 0: self._activepagename = unicode(INFO[index][0]['name']) stackindex = self.pageListIndexToStack.get(index, -1) if stackindex >= 0: self.stack.setCurrentIndex(stackindex) return item = self.pageList.item(index) for data in INFO: if item.text() == data[0]['label']: meta, info = data break stackindex = self.stack.count() pagename = meta['name'] page = self.createPage(pagename, info) self.refreshPage(page) # better to call stack.addWidget() here, not by fillFrame() assert self.stack.count() > stackindex, 'page must be added to stack' self.pageListIndexToStack[index] = stackindex self.stack.setCurrentIndex(stackindex) def editClicked(self): 'Open internal editor in stacked widget' if self.isDirty(): ret = qtlib.CustomPrompt(_('Confirm Save'), _('Save changes before editing?'), self, (_('&Save'), _('&Discard'), _('Cancel')), default=2, esc=2).run() if ret == 0: self.applyChanges() elif ret == 2: return qscilib.fileEditor(hglib.tounicode(self.fn), foldable=True) self.refresh() def refresh(self, *args): # refresh config values self.ini = self.loadIniFile(self.rcpath) self.readonly = self.forcereadonly or not (hasattr(self.ini, 'write') and os.access(self.fn, os.W_OK)) self.stack.setDisabled(self.readonly) self.fnedit.setText(hglib.tounicode(self.fn)) for page in self.pages.values(): self.refreshPage(page) def refreshPage(self, page): name, info, widgets = page if name == 'extensions': for row, w in enumerate(widgets): key = w.opts['label'] for fullkey in (key, 'hgext.%s' % key, 'hgext/%s' % key): val = self.readCPath('extensions.' + fullkey) if val != None: break if val == None: curvalue = False elif len(val) and val[0] == '!': curvalue = False else: curvalue = True w.setValue(curvalue) if val == None: w.opts['cpath'] = 'extensions.' + key else: w.opts['cpath'] = 'extensions.' + fullkey self.validateextensions() elif name == 'tools': self.toolsFrame.refresh() elif name == 'hooks': self.hooksFrame.refresh() else: for row, e in enumerate(info): if not e.cpath: continue # a dummy field curvalue = self.readCPath(e.cpath) widgets[row].setValue(curvalue) def isDirty(self): if self.readonly: return False for name, info, widgets in self.pages.values(): for w in widgets: if w.isDirty(): return True return False def reloadClicked(self): if self.isDirty(): d = QMessageBox.question(self, _('Confirm Reload'), _('Unsaved changes will be lost.\n' 'Do you want to reload?'), QMessageBox.Ok | QMessageBox.Cancel) if d != QMessageBox.Ok: return self.refresh() def focusPage(self, focuspage): 'Set change page to focuspage' for i, (meta, info) in enumerate(INFO): if meta['name'] == focuspage: self._activepagename = meta['name'] self.pageList.setCurrentRow(i) return True return False def focusField(self, focusfield): 'Set page and focus to requested datum' if not focusfield: return False if focusfield.find('.') < 0: return self.focusPage(focusfield) for i, (meta, info) in enumerate(INFO): for n, e in enumerate(info): if e.cpath == focusfield: self.pageList.setCurrentRow(i) QTimer.singleShot(0, lambda: self.pages[meta['name']][2][n].setFocus()) return True return False def fillFrame(self, info): widgets = [] frame = QFrame() form = QFormLayout() form.setContentsMargins(5, 5, 0, 5) frame.setLayout(form) self.stack.addWidget(frame) for e in info: opts = {'label': e.label, 'cpath': e.cpath, 'tooltip': e.tooltip, 'master': e.master, 'settings':self.settings} if isinstance(e.values, tuple): func = e.values[0] w = func(opts, e.values[1]) else: func = e.values w = func(opts) if e.globalonly: w.setEnabled(self.rcpath == scmutil.userrcpath()) lbl = QLabel(e.label) lbl.setToolTip(e.tooltip) widgets.append(w) if e.isVisible(): lbl.installEventFilter(self) w.installEventFilter(self) form.addRow(lbl, w) # assign the master to widgets that have a master for w in widgets: if w.opts['master'] != None: for dep in widgets: if dep.opts['cpath'] == w.opts['master']: w.opts['master'] = dep return widgets def fillExtensionsFrame(self): widgets = [] frame = QFrame() grid = QGridLayout() grid.setContentsMargins(5, 5, 0, 5) frame.setLayout(grid) self.stack.addWidget(frame) allexts = hglib.allextensions() allextslist = list(allexts) MAXCOLUMNS = 3 maxrows = (len(allextslist) + MAXCOLUMNS - 1) / MAXCOLUMNS i = 0 extsinfo = () for i, name in enumerate(sorted(allexts)): tt = hglib.tounicode(allexts[name]) opts = {'label':name, 'cpath':'extensions.' + name, 'tooltip':tt} w = genCheckBox(opts) w.installEventFilter(self) w.clicked.connect(self.validateextensions) row, col = i / maxrows, i % maxrows grid.addWidget(w, col, row) widgets.append(w) return extsinfo, widgets def fillToolsFrame(self): self.toolsFrame = frame = customtools.ToolsFrame(self.ini, parent=self) self.stack.addWidget(frame) return (), [frame] def fillHooksFrame(self): self.hooksFrame = frame = customtools.HooksFrame(self.ini, parent=self) self.stack.addWidget(frame) return (), [frame] def eventFilter(self, obj, event): if event.type() in (QEvent.Enter, QEvent.FocusIn): self.desctext.setHtml(obj.toolTip()) if event.type() == QEvent.ToolTip: return True # tooltip is shown in self.desctext return False def createPage(self, name, info): if name == 'extensions': extsinfo, widgets = self.fillExtensionsFrame() self.pages[name] = name, extsinfo, widgets elif name == 'tools': toolsinfo, widgets = self.fillToolsFrame() self.pages[name] = name, toolsinfo, widgets elif name == 'hooks': hooksinfo, widgets = self.fillHooksFrame() self.pages[name] = name, hooksinfo, widgets else: widgets = self.fillFrame(info) self.pages[name] = name, info, widgets return self.pages[name] def readCPath(self, cpath): 'Retrieve a value from the parsed config file' # Presumes single section/key level depth section, key = cpath.split('.', 1) return self.ini.get(section, key) def loadIniFile(self, rcpath): for fn in rcpath: if os.path.exists(fn): break else: for fn in rcpath: # Try to create a file from rcpath try: f = open(fn, 'w') f.write('# Generated by TortoiseHg settings dialog\n') f.close() break except (IOError, OSError): pass else: qtlib.WarningMsgBox(_('Unable to create a Mercurial.ini file'), _('Insufficient access rights, reverting to read-only ' 'mode.'), parent=self) from mercurial import config self.fn = rcpath[0] return config.config() self.fn = fn return wconfig.readfile(self.fn) def recordNewValue(self, cpath, newvalue): """Set the given value to ini; returns True if changed""" # 'newvalue' is in local encoding section, key = cpath.split('.', 1) if newvalue == self.ini.get(section, key): return False if newvalue == None: try: del self.ini[section][key] except KeyError: pass else: self.ini.set(section, key, newvalue) return True def applyChanges(self): if self.readonly: return True # safely skipped because all fields are disabled for name, info, widgets in self.pages.values(): if name == 'extensions': self.applyChangesForExtensions() elif name == 'tools': self.applyChangesForTools() elif name == 'hooks': self.applyChangesForHooks() else: for row, e in enumerate(info): if not e.cpath: continue # a dummy field newvalue = widgets[row].value() changed = self.recordNewValue(e.cpath, newvalue) if changed and e.restartneeded: self.restartRequested.emit(e.label) try: wconfig.writefile(self.ini, self.fn) return True except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) return False def applyChangesForExtensions(self): emitChanged = False section = 'extensions' enabledexts = hglib.enabledextensions() for chk in self.pages['extensions'][2]: if (not emitChanged) and chk.isDirty(): self.restartRequested.emit(_('Extensions')) emitChanged = True name = chk.opts['label'] section, key = chk.opts['cpath'].split('.', 1) newvalue = chk.value() if newvalue and (name in enabledexts): continue # unchanged if newvalue: self.ini.set(section, key, '') else: try: del self.ini[section][key] except KeyError: pass @pyqtSlot() def validateextensions(self): section = 'extensions' enabledexts = hglib.enabledextensions() selectedexts = set(chk.opts['label'] for chk in self.pages['extensions'][2] if chk.isChecked()) invalidexts = hglib.validateextensions(selectedexts) def getinival(cpath): if section not in self.ini: return None sect, key = cpath.split('.', 1) try: return self.ini[sect][key] except KeyError: pass def changable(name, cpath): curval = getinival(cpath) if curval not in ('', None): # enabled or unspecified, official extensions only return False elif name in enabledexts and curval is None: # re-disabling ext is not supported return False elif name in invalidexts and name not in selectedexts: # disallow to enable bad exts, but allow to disable it return False else: return True allexts = hglib.allextensions() for chk in self.pages['extensions'][2]: name = chk.opts['label'] if not changable(name, chk.opts['cpath']): chk.setEnabled(False) cpath = chk.opts['cpath'] sect, key = cpath.split('.', 1) if ui.ui().config(sect, key, None) is not None: chk.setValue(True) chk.curvalue = True else: chk.setEnabled(True) invalmsg = invalidexts.get(name) if invalmsg: invalmsg = invalmsg.decode('utf-8') chk.setToolTip(invalmsg or hglib.tounicode(allexts[name])) def applyChangesForTools(self): if self.toolsFrame.applyChanges(self.ini): self.restartRequested.emit(_('Tools')) def applyChangesForHooks(self): if self.hooksFrame.applyChanges(self.ini): self.restartRequested.emit(_('Hooks')) tortoisehg-2.10/tortoisehg/hgqt/bisect.py0000644000076400007640000001374012231647662017664 0ustar stevesteve# bisect.py - Bisect dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util, error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, qtlib class BisectDialog(QDialog): def __init__(self, repoagent, opts, parent=None): super(BisectDialog, self).__init__(parent) self.setWindowTitle(_('Bisect - %s') % repoagent.rawRepo().displayname) self.setWindowIcon(qtlib.geticon('hg-bisect')) self.setWindowFlags(Qt.Window) self._repoagent = repoagent # base layout box box = QVBoxLayout() box.setSpacing(6) self.setLayout(box) hbox = QHBoxLayout() hbox.addWidget(QLabel(_('Known good revision:'))) self._gle = gle = QLineEdit() gle.setText(opts.get('good', '')) hbox.addWidget(gle, 1) self._gb = gb = QPushButton(_('Accept')) hbox.addWidget(gb) box.addLayout(hbox) hbox = QHBoxLayout() hbox.addWidget(QLabel(_('Known bad revision:'))) self._ble = ble = QLineEdit() ble.setText(opts.get('bad', '')) ble.setEnabled(False) hbox.addWidget(ble, 1) self._bb = bb = QPushButton(_('Accept')) bb.setEnabled(False) hbox.addWidget(bb) box.addLayout(hbox) ## command widget self.cmd = cmdui.Widget(True, True, self) self.cmd.setShowOutput(True) box.addWidget(self.cmd, 1) hbox = QHBoxLayout() goodrev = QPushButton(_('Revision is Good')) hbox.addWidget(goodrev) badrev = QPushButton(_('Revision is Bad')) hbox.addWidget(badrev) skiprev = QPushButton(_('Skip this Revision')) hbox.addWidget(skiprev) box.addLayout(hbox) hbox = QHBoxLayout() box.addLayout(hbox) self._lbl = lbl = QLabel() hbox.addWidget(lbl) hbox.addStretch(1) closeb = QPushButton(_('Close')) hbox.addWidget(closeb) closeb.clicked.connect(self.reject) self.nextbuttons = (goodrev, badrev, skiprev) for b in self.nextbuttons: b.setEnabled(False) self.lastrev = None self.cmd.commandFinished.connect(self._cmdFinished) gb.pressed.connect(self._verifyGood) bb.pressed.connect(self._verifyBad) gle.returnPressed.connect(self._verifyGood) ble.returnPressed.connect(self._verifyBad) goodrev.clicked.connect(self._markGoodRevision) badrev.clicked.connect(self._markBadRevision) skiprev.clicked.connect(self._skipRevision) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.reject() super(BisectDialog, self).keyPressEvent(event) @property def repo(self): return self._repoagent.rawRepo() def _bisectcmd(self, *args, **opts): opts['repository'] = self.repo.root return hglib.buildcmdargs('bisect', *args, **opts) @pyqtSlot(int) def _cmdFinished(self, ret): lbl = self._lbl if ret != 0: lbl.setText(_('Error encountered.')) return self.repo.dirstate.invalidate() ctx = self.repo['.'] if ctx.rev() == self.lastrev: lbl.setText(_('Culprit found.')) return self.lastrev = ctx.rev() for b in self.nextbuttons: b.setEnabled(True) lbl.setText('%s: %d (%s) -> %s' % (_('Revision'), ctx.rev(), ctx, _('Test this revision and report findings. ' '(good/bad/skip)'))) @pyqtSlot() def _verifyGood(self): good = hglib.fromunicode(self._gle.text().simplified()) try: ctx = self.repo[good] self.goodrev = ctx.rev() self._gb.setEnabled(False) self._gle.setEnabled(False) self._bb.setEnabled(True) self._ble.setEnabled(True) self._ble.setFocus() except (error.LookupError, error.RepoLookupError), e: self.cmd.core.stbar.showMessage(hglib.tounicode(str(e))) except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) self.cmd.core.stbar.showMessage(err) @pyqtSlot() def _verifyBad(self): bad = hglib.fromunicode(self._ble.text().simplified()) try: ctx = self.repo[bad] self.badrev = ctx.rev() self._ble.setEnabled(False) self._bb.setEnabled(False) cmds = [] cmds.append(self._bisectcmd(reset=True)) cmds.append(self._bisectcmd(self.goodrev, good=True)) cmds.append(self._bisectcmd(self.badrev, bad=True)) self.cmd.run(*cmds) except (error.LookupError, error.RepoLookupError), e: self.cmd.core.stbar.showMessage(hglib.tounicode(str(e))) except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) self.cmd.core.stbar.showMessage(err) @pyqtSlot() def _markGoodRevision(self): for b in self.nextbuttons: b.setEnabled(False) self.cmd.run(self._bisectcmd('.', good=True)) @pyqtSlot() def _markBadRevision(self): for b in self.nextbuttons: b.setEnabled(False) self.cmd.run(self._bisectcmd('.', bad=True)) @pyqtSlot() def _skipRevision(self): for b in self.nextbuttons: b.setEnabled(False) self.cmd.run(self._bisectcmd('.', skip=True)) tortoisehg-2.10/tortoisehg/hgqt/mqutil.py0000644000076400007640000000614712231647662017731 0ustar stevesteve# mqutil.py - Common functionality for TortoiseHg MQ widget # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os, re, time from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util from hgext import mq as mqmod from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, rejects def getQQueues(repo): ui = repo.ui.copy() ui.quiet = True # don't append "(active)" ui.pushbuffer() try: opts = {'list': True} mqmod.qqueue(ui, repo, None, **opts) qqueues = hglib.tounicode(ui.popbuffer()).splitlines() except (util.Abort, EnvironmentError): qqueues = [] return qqueues def defaultNewPatchName(repo): t = time.strftime('%Y-%m-%d_%H-%M-%S') return t + '_r%d+.diff' % repo['.'].rev() def getPatchNameLineEdit(): patchNameLE = QLineEdit() if hasattr(patchNameLE, 'setPlaceholderText'): # Qt >= 4.7 patchNameLE.setPlaceholderText(_('### patch name ###')) return patchNameLE def getUserOptions(opts, *optionlist): out = [] for opt in optionlist: if opt not in opts: continue val = opts[opt] if val is False: continue elif val is True: out.append('--' + opt) else: out.append('--' + opt) out.append(val) return out def checkForRejects(repo, rawoutput, parent=None): """Parse output of qpush/qpop to resolve hunk failure manually""" rejre = re.compile('saving rejects to file (.*).rej') rejfiles = [m.group(1) for m in rejre.finditer(rawoutput) if os.path.exists(repo.wjoin(m.group(1)))] for wfile in rejfiles: ufile = hglib.tounicode(wfile) if qtlib.QuestionMsgBox(_('Manually resolve rejected chunks?'), _('%s had rejected chunks, edit patched ' 'file together with rejects?') % ufile, parent=parent): dlg = rejects.RejectsDialog(repo.ui, repo.wjoin(wfile), parent) dlg.exec_() return len(rejfiles) def mqNewRefreshCommand(repo, isnew, stwidget, pnwidget, message, opts, olist): if isnew: name = hglib.fromunicode(pnwidget.text()) if not name: qtlib.ErrorMsgBox(_('Patch Name Required'), _('You must enter a patch name')) pnwidget.setFocus() return cmdline = ['qnew', name] else: cmdline = ['qrefresh'] if message: cmdline += ['--message=' + hglib.fromunicode(message)] cmdline += getUserOptions(opts, *olist) files = ['--'] + [repo.wjoin(x) for x in stwidget.getChecked()] addrem = [repo.wjoin(x) for x in stwidget.getChecked('!?')] if len(files) > 1: cmdline += files else: cmdline += ['--exclude', repo.root] if addrem: cmdlines = [ ['addremove'] + addrem, cmdline] else: cmdlines = [cmdline] return cmdlines tortoisehg-2.10/tortoisehg/hgqt/filelistmodel.py0000644000076400007640000002163512235634453021247 0ustar stevesteve# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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. from tortoisehg.util import hglib, patchctx from tortoisehg.hgqt.qtlib import geticon, getoverlaidicon from PyQt4.QtCore import * from PyQt4.QtGui import * nullvariant = QVariant() def getSubrepoIcoDict(baseicon='thg-subrepo'): 'Return a dictionary mapping each subrepo type to the corresponding icon' _subrepoType2IcoMap = { 'hg': 'hg', 'git': 'thg-git-subrepo', 'svn': 'thg-svn-subrepo', 'hgsubversion': 'thg-svn-subrepo', 'empty': 'hg' } icOverlay = geticon(baseicon) subrepoIcoDict = {} for stype in _subrepoType2IcoMap: ic = geticon(_subrepoType2IcoMap[stype]) ic = getoverlaidicon(ic, icOverlay) subrepoIcoDict[stype] = ic return subrepoIcoDict class HgFileListModel(QAbstractTableModel): """ Model used for listing (modified) files of a given Hg revision """ showMessage = pyqtSignal(QString) def __init__(self, parent): QAbstractTableModel.__init__(self, parent) self._boldfont = parent.font() self._boldfont.setBold(True) self._ctx = None self._files = [] self._filesdict = {} self._fulllist = False self._subrepoIcoDict = { 'M': getSubrepoIcoDict(), 'A': getSubrepoIcoDict('thg-added-subrepo'), 'R': getSubrepoIcoDict('thg-removed-subrepo'), } @pyqtSlot(bool) def toggleFullFileList(self, value): self._fulllist = value self.loadFiles() self.layoutChanged.emit() def __len__(self): return len(self._files) def rowCount(self, parent=None): return len(self) def columnCount(self, parent=None): return 1 def file(self, row): return self._files[row]['path'] def setContext(self, ctx): reload = False if not self._ctx: reload = True elif self._ctx.rev() is None: reload = True elif ctx.thgid() != self._ctx.thgid(): reload = True if reload: self._ctx = ctx self.loadFiles() self.layoutChanged.emit() def fileFromIndex(self, index): if not index.isValid() or index.row()>=len(self) or not self._ctx: return None row = index.row() return self._files[row]['path'] def dataFromIndex(self, index): if not index.isValid() or index.row()>=len(self) or not self._ctx: return None row = index.row() return self._files[row] def indexFromFile(self, filename): if filename in self._filesdict: try: row = self._files.index(self._filesdict[filename]) return self.index(row, 0) except ValueError: pass return QModelIndex() def setFilter(self, match): 'simple match in filename filter' self.layoutAboutToBeChanged.emit() oldindexes = [(self.indexFromFile(r['path']), r['path']) for r in self._files] self._files = [r for r in self._unfilteredfiles if hglib.fromunicode(match) in r['path']] for oi, filename in oldindexes: self.changePersistentIndex(oi, self.indexFromFile(filename)) self.layoutChanged.emit() def _buildDesc(self, parent): files = [] ctxfiles = self._ctx.files() modified, added, removed = self._ctx.changesToParent(parent) ismerge = bool(self._ctx.p2()) # Add the list of modified subrepos to the top of the list if not isinstance(self._ctx, patchctx.patchctx): if ".hgsubstate" in ctxfiles or ".hgsub" in ctxfiles: from mercurial import subrepo # Add the list of modified subrepos for s, sd in self._ctx.substate.items(): srev = self._ctx.substate.get(s, subrepo.nullstate)[1] stype = self._ctx.substate.get(s, subrepo.nullstate)[2] sp1rev = self._ctx.p1().substate.get(s, subrepo.nullstate)[1] sp2rev = '' if ismerge: sp2rev = self._ctx.p2().substate.get(s, subrepo.nullstate)[1] if srev != sp1rev or (sp2rev != '' and srev != sp2rev): wasmerged = ismerge and s in ctxfiles substatus = 'M' if sp1rev == '': substatus = 'A' files.append({ 'path': s, 'status': 'S', 'parent': parent, 'stype': stype, 'wasmerged': wasmerged, 'substatus': substatus, }) # Add the list of missing subrepos subreposet = set(self._ctx.substate.keys()) subrepoparent1set = set(self._ctx.p1().substate.keys()) missingsubreposet = subrepoparent1set.difference(subreposet) for s in missingsubreposet: wasmerged = ismerge and s in ctxfiles stype = self._ctx.p1().substate.get(s, subrepo.nullstate)[2] files.append({ 'path': s, 'status': 'S', 'parent': parent, 'stype': stype, 'wasmerged': wasmerged, 'substatus': 'R', }) if self._fulllist and ismerge: func = lambda x: True else: func = lambda x: x in ctxfiles for lst, flag in ((added, 'A'), (modified, 'M'), (removed, 'R')): for f in filter(func, lst): wasmerged = ismerge and f in ctxfiles f = self._ctx.removeStandin(f) files.append({'path': f, 'status': flag, 'parent': parent, 'wasmerged': wasmerged}) return files def loadFiles(self): self._files = [] try: self._files = self._buildDesc(0) if bool(self._ctx.p2()): _paths = [x['path'] for x in self._files] _files = self._buildDesc(1) self._files += [x for x in _files if x['path'] not in _paths] except EnvironmentError, e: self.showMessage.emit(hglib.tounicode(str(e))) # Make a "copy" of self._files that can be used as the data source # when filtering. Note that there is no need for this to be an actual # copy, because self._files will be recreated on setFilter() self._unfilteredfiles = self._files self._filesdict = dict([(f['path'], f) for f in self._files]) def data(self, index, role): if not index.isValid() or index.row()>len(self) or not self._ctx: return nullvariant if index.column() != 0: return nullvariant row = index.row() column = index.column() current_file_desc = self._files[row] current_file = current_file_desc['path'] if role in (Qt.DisplayRole, Qt.ToolTipRole): return QVariant(hglib.tounicode(current_file)) elif role == Qt.DecorationRole: if self._fulllist and bool(self._ctx.p2()): if current_file_desc['wasmerged']: icn = geticon('thg-file-merged') elif current_file_desc['parent'] == 0: icn = geticon('thg-file-p0') elif current_file_desc['parent'] == 1: icn = geticon('thg-file-p1') return QVariant(icn.pixmap(20,20)) elif current_file_desc['status'] == 'A': return QVariant(geticon('fileadd')) elif current_file_desc['status'] == 'R': return QVariant(geticon('filedelete')) elif current_file_desc['status'] == 'S': stype = current_file_desc.get('stype', 'hg') substatus = current_file_desc['substatus'] return QVariant(self._subrepoIcoDict[substatus][stype]) #else: # return QVariant(geticon('filemodify')) elif role == Qt.FontRole: if current_file_desc['wasmerged']: return QVariant(self._boldfont) else: return nullvariant tortoisehg-2.10/tortoisehg/hgqt/repowidget.py0000644000076400007640000026004512231647662020566 0ustar stevesteve# repowidget.py - TortoiseHg repository widget # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright (C) 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import binascii import os import shlex, subprocess # used by runCustomCommand import urllib import cStringIO from mercurial import revset, error, patch, phases, util, ui from tortoisehg.util import hglib, shlib, paths from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.qtlib import QuestionMsgBox, InfoMsgBox, WarningMsgBox from tortoisehg.hgqt.qtlib import DemandWidget from tortoisehg.hgqt.repomodel import HgRepoListModel from tortoisehg.hgqt import cmdui, update, tag, backout, merge, visdiff from tortoisehg.hgqt import archive, thgimport, thgstrip, purge, bookmark from tortoisehg.hgqt import bisect, rebase, resolve, thgrepo, compress, mq from tortoisehg.hgqt import shelve from tortoisehg.hgqt import matching, graft, hgemail, postreview from tortoisehg.hgqt import sign from tortoisehg.hgqt.repofilter import RepoFilterBar from tortoisehg.hgqt.repoview import HgRepoView from tortoisehg.hgqt.revdetails import RevDetailsWidget from tortoisehg.hgqt.commit import CommitWidget from tortoisehg.hgqt.manifestdialog import ManifestDialog, ManifestWidget from tortoisehg.hgqt.sync import SyncWidget from tortoisehg.hgqt.grep import SearchWidget from tortoisehg.hgqt.pbranch import PatchBranchWidget from PyQt4.QtCore import * from PyQt4.QtGui import * class RepoWidget(QWidget): showMessageSignal = pyqtSignal(QString) toolbarVisibilityChanged = pyqtSignal() # TODO: output and progress can be removed if all widgets use RepoAgent output = pyqtSignal(QString, QString) progress = pyqtSignal(QString, object, QString, QString, object) makeLogVisible = pyqtSignal(bool) revisionSelected = pyqtSignal(object) titleChanged = pyqtSignal(unicode) """Emitted when changed the expected title for the RepoWidget tab""" busyIconChanged = pyqtSignal() repoLinkClicked = pyqtSignal(unicode) """Emitted when clicked a link to open repository""" def __init__(self, repoagent, parent=None, bundle=None): QWidget.__init__(self, parent, acceptDrops=True) self._repoagent = repoagent # TODO: use _repoagent where appropriate self.repo = repo = repoagent.rawRepo() repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.configChanged.connect(self.configChanged) self.revsetfilter = False self.bundle = None # bundle file name [local encoding] self.bundlesource = None # source URL of incoming bundle [unicode] self.outgoingMode = False self.revset = [] self._busyIconNames = [] self.namedTabs = {} self.repolen = len(repo) self.destroyed.connect(self.repo.thginvalidate) # Determine the "initial revision" that must be shown when # opening the repo. # The "initial revision" can be selected via the settings, and it can # have 3 possible values: # - "current": Select the current (i.e. working dir parent) revision # - "tip": Select tip of the repository # - "workingdir": Select the working directory pseudo-revision initialRevision = \ self.repo.ui.config('tortoisehg', 'initialrevision', 'current').lower() initialRevisionDict = { 'current': '.', 'tip': 'tip', 'workingdir': None } if initialRevision in initialRevisionDict: default_rev = initialRevisionDict[initialRevision] else: # By default we'll select the current (i.e. working dir parent) revision default_rev = '.' if repo.parents()[0].rev() == -1: self._reload_rev = 'tip' else: self._reload_rev = default_rev self.currentMessage = '' self.dirty = False self.setupUi() self.createActions() self.loadSettings() self.setupModels() if bundle: self.setBundle(bundle) self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self) # Select the widget chosen by the user defaultWidget = \ self.repo.ui.config( 'tortoisehg', 'defaultwidget', 'revdetails').lower() widgetDict = { 'revdetails': self.logTabIndex, 'commit': self.commitTabIndex, 'sync': self.syncTabIndex, 'manifest': self.manifestTabIndex, 'search': self.grepTabIndex } if initialRevision == 'workingdir': # Do not allow selecting the revision details widget when the # selected revision is the working directory pseudo-revision widgetDict['revdetails'] = self.commitTabIndex if defaultWidget in widgetDict: widgetIndex = widgetDict[defaultWidget] # Note: if the mq extension is not enabled, self.mqTabIndex will # be negative if widgetIndex > 0: self.taskTabsWidget.setCurrentIndex(widgetIndex) self.output.connect(self._showOutputOnInfoBar) def setupUi(self): SP = QSizePolicy self.repotabs_splitter = QSplitter(orientation=Qt.Vertical) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self._activeInfoBar = None self.filterbar = RepoFilterBar(self.repo, self) self.layout().addWidget(self.filterbar) self.filterbar.branchChanged.connect(self.setBranch) self.filterbar.showHiddenChanged.connect(self.setShowHidden) self.filterbar.showGraftSourceChanged.connect(self.setShowGraftSource) self.filterbar.progress.connect(self.progress) self.filterbar.showMessage.connect(self.showMessage) self.filterbar.showMessage.connect(self._showMessageOnInfoBar) self.filterbar.setRevisionSet.connect(self.setRevisionSet) self.filterbar.clearRevisionSet.connect(self._unapplyRevisionSet) self.filterbar.filterToggled.connect(self.filterToggled) self.filterbar.hide() self.revsetfilter = self.filterbar.filtercb.isChecked() self.layout().addWidget(self.repotabs_splitter) cs = ('workbench', _('Workbench Log Columns')) self.repoview = view = HgRepoView(self.repo, 'repoWidget', cs, self) view.revisionClicked.connect(self.onRevisionClicked) view.revisionSelected.connect(self.onRevisionSelected) view.revisionAltClicked.connect(self.onRevisionSelected) view.revisionActivated.connect(self.onRevisionActivated) view.showMessage.connect(self.showMessage) view.menuRequested.connect(self.viewMenuRequest) sp = SP(SP.Expanding, SP.Expanding) sp.setHorizontalStretch(0) sp.setVerticalStretch(1) sp.setHeightForWidth(self.repoview.sizePolicy().hasHeightForWidth()) view.setSizePolicy(sp) view.setFrameShape(QFrame.StyledPanel) self.repotabs_splitter.addWidget(self.repoview) self.repotabs_splitter.setCollapsible(0, True) self.repotabs_splitter.setStretchFactor(0, 1) self.taskTabsWidget = tt = QTabWidget() self.repotabs_splitter.addWidget(self.taskTabsWidget) self.repotabs_splitter.setStretchFactor(1, 1) tt.setDocumentMode(True) self.updateTaskTabs() self.revDetailsWidget = w = RevDetailsWidget(self._repoagent, self) self.revDetailsWidget.filelisttbar.setStyleSheet(qtlib.tbstylesheet) w.linkActivated.connect(self._openLink) w.revisionSelected.connect(self.repoview.goto) w.grepRequested.connect(self.grep) w.showMessage.connect(self.showMessage) w.updateToRevision.connect(self.updateToRevision) w.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) self.logTabIndex = idx = tt.addTab(w, qtlib.geticon('hg-log'), '') self.namedTabs['log'] = idx tt.setTabToolTip(idx, _("Revision details", "tab tooltip")) self.commitDemand = w = DemandWidget('createCommitWidget', self) self.commitTabIndex = idx = tt.addTab(w, qtlib.geticon('hg-commit'), '') self.namedTabs['commit'] = idx tt.setTabToolTip(idx, _("Commit", "tab tooltip")) self.syncDemand = w = DemandWidget('createSyncWidget', self) self.syncTabIndex = idx = tt.addTab(w, qtlib.geticon('thg-sync'), '') self.namedTabs['sync'] = idx tt.setTabToolTip(idx, _("Synchronize", "tab tooltip")) self.manifestDemand = w = DemandWidget('createManifestWidget', self) self.manifestTabIndex = idx = tt.addTab(w, qtlib.geticon('hg-annotate'), '') self.namedTabs['manifest'] = idx tt.setTabToolTip(idx, _('Manifest', "tab tooltip")) self.grepDemand = w = DemandWidget('createGrepWidget', self) self.grepTabIndex = idx = tt.addTab(w, qtlib.geticon('hg-grep'), '') self.namedTabs['grep'] = idx tt.setTabToolTip(idx, _("Search", "tab tooltip")) if 'pbranch' in self.repo.extensions(): self.pbranchDemand = w = DemandWidget('createPatchBranchWidget', self) self.pbranchTabIndex = idx = tt.addTab(w, qtlib.geticon('branch'), '') tt.setTabToolTip(idx, _("Patch Branch", "tab tooltip")) self.namedTabs['pbranch'] = idx else: self.pbranchTabIndex = -1 @pyqtSlot(QString) def switchToNamedTaskTab(self, tabname): tabname = str(tabname) if tabname in self.namedTabs: idx = self.namedTabs[tabname] # refresh status even if current widget is already a 'commit' if (tabname == 'commit' and self.taskTabsWidget.currentIndex() == idx): self._refreshCommitTabIfNeeded() self.taskTabsWidget.setCurrentIndex(idx) # restore default splitter position if task tab is invisible if self.repotabs_splitter.sizes()[1] == 0: self.repotabs_splitter.setSizes([1, 1]) def repoRootPath(self): return self._repoagent.rootPath() def title(self): """Returns the expected title for this widget [unicode]""" if self.bundle: return _('%s ') % self.repo.shortname elif self.repomodel.branch(): return u'%s [%s]' % (self.repo.shortname, self.repomodel.branch()) else: return self.repo.shortname def busyIcon(self): if self._busyIconNames: return qtlib.geticon(self._busyIconNames[-1]) else: return QIcon() def filterBarVisible(self): return self.filterbar.isVisible() @pyqtSlot(bool) def toggleFilterBar(self, checked): """Toggle display repowidget filter bar""" self.filterbar.setVisible(checked) def _openRepoLink(self, upath): path = hglib.fromunicode(upath) if not os.path.isabs(path): path = self.repo.wjoin(path) self.repoLinkClicked.emit(hglib.tounicode(path)) @pyqtSlot(unicode) def _openLink(self, link): link = unicode(link) handlers = {'cset': self.goto, 'log': lambda a: self.makeLogVisible.emit(True), 'repo': self._openRepoLink, 'shelve' : self.shelve} if ':' in link: scheme, param = link.split(':', 1) hdr = handlers.get(scheme) if hdr: return hdr(param) if os.path.isabs(link): qtlib.openlocalurl(link) else: QDesktopServices.openUrl(QUrl(link)) def setInfoBar(self, cls, *args, **kwargs): """Show the given infobar at top of RepoWidget If the priority of the current infobar is higher than new one, the request is silently ignored. """ cleared = self.clearInfoBar(priority=cls.infobartype) if not cleared: return w = cls(*args, **kwargs) w.setParent(self) w.finished.connect(self._freeInfoBar) w.linkActivated.connect(self._openLink) self._activeInfoBar = w self._updateInfoBarGeometry() w.show() if w.infobartype > qtlib.InfoBar.INFO: w.setFocus() # to handle key press by InfoBar return w @pyqtSlot() def clearInfoBar(self, priority=None): """Close current infobar if available; return True if got empty""" if not self._activeInfoBar: return True if priority is None or self._activeInfoBar.infobartype <= priority: self._activeInfoBar.finished.disconnect(self._freeInfoBar) self._activeInfoBar.close() self._freeInfoBar() # call directly in case of event delay return True else: return False @pyqtSlot() def _freeInfoBar(self): """Disown closed infobar""" if not self._activeInfoBar: return self._activeInfoBar.setParent(None) self._activeInfoBar = None # clear margin for overlay h = self.repoview.horizontalHeader() if h.minimumSize() != QSize(0, 0): h.setMinimumSize(0, 0) h.geometriesChanged.emit() def _updateInfoBarGeometry(self): if not self._activeInfoBar: return w = self._activeInfoBar top = self.repoview.mapTo(self, QPoint(0, 0)).y() w.setGeometry(0, top, self.width(), w.heightForWidth(self.width())) # give margin to make header or first row accessible. without header, # column width cannot be changed while confirmation is presented. if w.infobartype > qtlib.InfoBar.INFO: h = self.repoview.horizontalHeader() y = h.mapTo(self.repoview, QPoint(0, 0)).y() if w.infobartype >= qtlib.InfoBar.CONFIRM: xh = h.sizeHint().height() else: xh = 0 h.setMinimumSize(0, max(w.height() - y, 0) + xh) h.geometriesChanged.emit() @pyqtSlot(unicode, unicode) def _showOutputOnInfoBar(self, msg, label, maxlines=2, maxwidth=140): labelslist = unicode(label).split() if 'ui.error' in labelslist: # Check if a subrepo is set in the label list subrepo = None subrepolabel = 'subrepo=' for label in labelslist: if label.startswith(subrepolabel): # The subrepo "label" is encoded ascii subrepo = hglib.tounicode( urllib.unquote(str(label)[len(subrepolabel):])) break # Limit the text shown on the info bar to maxlines lines of up to maxwidth chars msglines = unicode(msg).strip().splitlines() infolines = [] for line in msglines[0:maxlines]: if len(line) > maxwidth: line = line[0:maxwidth] + ' ...' infolines.append(line) if len(msglines) > maxlines and not infolines[-1].endswith('...'): infolines[-1] += ' ...' infomsg = qtlib.linkifyMessage('\n'.join(infolines), subrepo=subrepo) self.setInfoBar(qtlib.CommandErrorInfoBar, infomsg) @pyqtSlot(unicode) def _showMessageOnInfoBar(self, msg): if msg: self.setInfoBar(qtlib.StatusInfoBar, msg) else: self.clearInfoBar(priority=qtlib.StatusInfoBar.infobartype) def createCommitWidget(self): pats, opts = {}, {} cw = CommitWidget(self._repoagent, pats, opts, self, rev=self.rev) cw.buttonHBox.addWidget(cw.commitSetupButton()) cw.loadSettings(QSettings(), 'workbench') cw.progress.connect(self.progress) cw.linkActivated.connect(self._openLink) cw.showMessage.connect(self.showMessage) cw.grepRequested.connect(self.grep) cw.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) QTimer.singleShot(0, self._initCommitWidgetLate) return cw @pyqtSlot() def _initCommitWidgetLate(self): cw = self.commitDemand.get() cw.reload() # auto-refresh should be enabled after initial reload(); otherwise # refreshWctx() can be doubled self.taskTabsWidget.currentChanged.connect( self._refreshCommitTabIfNeeded) def createManifestWidget(self): if isinstance(self.rev, basestring): rev = None else: rev = self.rev w = ManifestWidget(self._repoagent, rev, self) w.loadSettings(QSettings(), 'workbench') w.revChanged.connect(self.repoview.goto) w.linkActivated.connect(self._openLink) w.showMessage.connect(self.showMessage) w.grepRequested.connect(self.grep) w.revsetFilterRequested.connect(self.setFilter) w.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) return w def createSyncWidget(self): sw = SyncWidget(self._repoagent, self) sw.output.connect(self.output) sw.progress.connect(self.progress) sw.makeLogVisible.connect(self.makeLogVisible) sw.syncStarted.connect(self.clearInfoBar) sw.outgoingNodes.connect(self.setOutgoingNodes) sw.showMessage.connect(self.showMessage) sw.showMessage.connect(self._showMessageOnInfoBar) sw.incomingBundle.connect(self.setBundle) sw.pullCompleted.connect(self.onPullCompleted) sw.pushCompleted.connect(self.pushCompleted) sw.showBusyIcon.connect(self.onShowBusyIcon) sw.hideBusyIcon.connect(self.onHideBusyIcon) sw.refreshTargets(self.rev) sw.switchToRequest.connect(self.switchToNamedTaskTab) return sw @pyqtSlot(QString) def onShowBusyIcon(self, iconname): self._busyIconNames.append(iconname) self.busyIconChanged.emit() @pyqtSlot(QString) def onHideBusyIcon(self, iconname): if iconname in self._busyIconNames: self._busyIconNames.remove(iconname) self.busyIconChanged.emit() @pyqtSlot(QString) def setFilter(self, filter): self.filterbar.setQuery(filter) self.filterbar.setVisible(True) self.filterbar.runQuery() @pyqtSlot(QString, QString) def setBundle(self, bfile, bsource=None): if self.bundle: self.clearBundle() self.bundle = hglib.fromunicode(bfile) self.bundlesource = bsource and unicode(bsource) or None oldlen = len(self.repo) self.repo = thgrepo.repository(self.repo.ui, self.repo.root, bundle=self.bundle) self.repoview.setRepo(self.repo) self.revDetailsWidget.setRepo(self.repo) self.manifestDemand.forward('setRepo', self.repo) self.filterbar.setQuery('incoming()') self.filterbar.setEnableFilter(False) self.titleChanged.emit(self.title()) newlen = len(self.repo) self.revset = range(oldlen, newlen) self.repomodel.setRevset(self.revset) self.reload(invalidate=False) self.repoview.resetBrowseHistory(self.revset) self._reload_rev = self.revset[0] w = self.setInfoBar(qtlib.ConfirmInfoBar, _('Found %d incoming changesets') % len(self.revset)) assert w w.acceptButton.setText(_('Accept')) w.acceptButton.setToolTip(_('Pull incoming changesets into ' 'your repository')) w.rejectButton.setText(_('Reject')) w.rejectButton.setToolTip(_('Reject incoming changesets')) w.accepted.connect(self.acceptBundle) w.rejected.connect(self.rejectBundle) def clearBundle(self): self.filterbar.setEnableFilter(True) self.filterbar.setQuery('') self.revset = [] self.repomodel.setRevset(self.revset) self.repoview.enablefilterpalette(False) self.bundle = None self.bundlesource = None self.titleChanged.emit(self.title()) self.repo = thgrepo.repository(self.repo.ui, self.repo.root) self.repoview.setRepo(self.repo) self.revDetailsWidget.setRepo(self.repo) self.manifestDemand.forward('setRepo', self.repo) def onPullCompleted(self): if self.bundle: self.clearBundle() self.reload(invalidate=False) def acceptBundle(self): if self.bundle: self.taskTabsWidget.setCurrentIndex(self.syncTabIndex) self.syncDemand.pullBundle(self.bundle, None, self.bundlesource) def pullBundleToRev(self): if self.bundle: # manually remove infobar to work around unwanted rejectBundle # during pull operation (issue #2596) if self._activeInfoBar: self._activeInfoBar.hide() self._freeInfoBar() self.taskTabsWidget.setCurrentIndex(self.syncTabIndex) self.syncDemand.pullBundle(self.bundle, self.rev, self.bundlesource) def rejectBundle(self): self.clearBundle() self.reload(invalidate=False) @pyqtSlot() def clearRevisionSet(self): self.filterbar.setQuery('') return self._unapplyRevisionSet() @pyqtSlot() def _unapplyRevisionSet(self): self.toolbarVisibilityChanged.emit() self.outgoingMode = False self.repoview.enablefilterpalette(False) if not self.revset: return False self.revset = [] if self.revsetfilter: self.reload() return True else: self.repomodel.setRevset([]) self.refresh() return False def setRevisionSet(self, revisions): revs = revisions[:] revs.sort(reverse=True) self.revset = revs if self.revsetfilter: self.reload() else: self.repomodel.setRevset(self.revset) self.refresh() self.repoview.resetBrowseHistory(self.revset) self._reload_rev = self.revset[0] self.repoview._paletteswitcher.enablefilterpalette(revs) self.clearInfoBar(qtlib.InfoBar.INFO) # clear progress message @pyqtSlot(bool) def filterToggled(self, checked): self.revsetfilter = checked if self.revset: self.repomodel.filterbyrevset = checked self.reload() self._resetBrowseHistoryOnFilterChange() def _resetBrowseHistoryOnFilterChange(self): if self.revset: self.repoview.resetBrowseHistory(self.revset, self.rev) def setOutgoingNodes(self, nodes): self.filterbar.setQuery('outgoing()') revs = [self.repo[n].rev() for n in nodes] self.setRevisionSet(revs) self.outgoingMode = True numnodes = len(nodes) numoutgoing = numnodes if self.syncDemand.get().isTargetSelected(): # Outgoing preview is already filtered by target selection defaultpush = None else: # Read the tortoisehg.defaultpush setting to determine what to push # by default, and set the button label and action accordingly defaultpush = self.repo.ui.config('tortoisehg', 'defaultpush', 'all') rev = None branch = None pushall = False # note that we assume that none of the revisions # on the nodes/revs lists is secret if defaultpush == 'branch': branch = self.repo['.'].branch() ubranch = hglib.tounicode(branch) # Get the list of revs that will be actually pushed outgoingrevs = self.repo.revs('%ld and branch(.)', revs) numoutgoing = len(outgoingrevs) elif defaultpush == 'revision': rev = self.repo['.'].rev() # Get the list of revs that will be actually pushed # excluding (potentially) the current rev outgoingrevs = self.repo.revs('%ld and ::.', revs) numoutgoing = len(outgoingrevs) maxrev = rev if numoutgoing > 0: maxrev = max(outgoingrevs) else: pushall = True # Set the default acceptbuttontext # Note that the pushall case uses the default accept button text if branch is not None: acceptbuttontext = _('Push current branch (%s)') % ubranch elif rev is not None: if maxrev == rev: acceptbuttontext = _('Push up to current revision (#%d)') % rev else: acceptbuttontext = _('Push up to revision #%d') % maxrev else: acceptbuttontext = _('Push all') if numnodes == 0: msg = _('no outgoing changesets') elif numoutgoing == 0: if branch: msg = _('no outgoing changesets in current branch (%s) ' '/ %d in total') % (ubranch, numnodes) elif rev is not None: if maxrev == rev: msg = _('no outgoing changesets up to current revision ' '(#%d) / %d in total') % (rev, numnodes) else: msg = _('no outgoing changesets up to revision #%d ' '/ %d in total') % (maxrev, numnodes) elif numoutgoing == numnodes: # This case includes 'Push all' among others msg = _('%d outgoing changesets') % numoutgoing elif branch: msg = _('%d outgoing changesets in current branch (%s) ' '/ %d in total') % (numoutgoing, ubranch, numnodes) elif rev: if maxrev == rev: msg = _('%d outgoing changesets up to current revision (#%d) ' '/ %d in total') % (numoutgoing, rev, numnodes) else: msg = _('%d outgoing changesets up to revision #%d ' '/ %d in total') % (numoutgoing, maxrev, numnodes) else: # This should never happen but we leave this else clause # in case there is a flaw in the logic above (e.g. due to # a future change in the code) msg = _('%d outgoing changesets') % numoutgoing w = self.setInfoBar(qtlib.ConfirmInfoBar, msg.strip()) assert w if numoutgoing == 0: acceptbuttontext = _('Nothing to push') w.acceptButton.setEnabled(False) w.acceptButton.setText(acceptbuttontext) w.accepted.connect(lambda: self.push(False, rev=rev, branch=branch, pushall=pushall)) # TODO: to the same URL w.rejected.connect(self.clearRevisionSet) def createGrepWidget(self): upats = {} gw = SearchWidget(self._repoagent, upats, self) gw.setRevision(self.repoview.current_rev) gw.showMessage.connect(self.showMessage) gw.progress.connect(self.progress) gw.revisionSelected.connect(self.goto) return gw def createPatchBranchWidget(self): pbw = PatchBranchWidget(self._repoagent, parent=self) return pbw @property def rev(self): """Returns the current active revision""" return self.repoview.current_rev def showMessage(self, msg): self.currentMessage = msg if self.isVisible(): self.showMessageSignal.emit(msg) def keyPressEvent(self, event): if self._activeInfoBar and event.key() == Qt.Key_Escape: self.clearInfoBar(qtlib.InfoBar.INFO) else: QWidget.keyPressEvent(self, event) def showEvent(self, event): QWidget.showEvent(self, event) self.showMessageSignal.emit(self.currentMessage) if self.dirty: print 'page was dirty, reloading...' self.reload() self.dirty = False def resizeEvent(self, event): QWidget.resizeEvent(self, event) self._updateInfoBarGeometry() def event(self, event): if event.type() == QEvent.LayoutRequest: self._updateInfoBarGeometry() return QWidget.event(self, event) def createActions(self): self._mqActions = None if 'mq' in self.repo.extensions(): self._mqActions = mq.PatchQueueActions(self) self._mqActions.setRepoAgent(self._repoagent) self.generateUnappliedPatchMenu() self.generateSingleMenu() self.generatePairMenu() self.generateMultipleSelectionMenu() self.generateBundleMenu() self.generateOutgoingMenu() def detectPatches(self, paths): filepaths = [] for p in paths: if not os.path.isfile(p): continue try: pf = open(p, 'rb') earlybytes = pf.read(4096) if '\0' in earlybytes: continue pf.seek(0) filename, message, user, date, branch, node, p1, p2 = \ patch.extract(self.repo.ui, pf) if filename: filepaths.append(p) os.unlink(filename) except Exception: pass return filepaths def dragEnterEvent(self, event): paths = [unicode(u.toLocalFile()) for u in event.mimeData().urls()] if self.detectPatches(paths): event.setDropAction(Qt.CopyAction) event.accept() def dropEvent(self, event): paths = [unicode(u.toLocalFile()) for u in event.mimeData().urls()] patches = self.detectPatches(paths) if not patches: return event.setDropAction(Qt.CopyAction) event.accept() self.thgimport(patches) ## Begin Workbench event forwards def back(self): self.repoview.back() def forward(self): self.repoview.forward() def bisect(self): dlg = bisect.BisectDialog(self._repoagent, {}, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def resolve(self): dlg = resolve.ResolveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def thgimport(self, paths=None): dlg = thgimport.ImportDialog(self._repoagent, self) dlg.patchImported.connect(self.gotoTip) if paths: dlg.setfilepaths(paths) dlg.exec_() def shelve(self, arg=None): self._dialogs.open(RepoWidget._createShelveDialog) def _createShelveDialog(self): dlg = shelve.ShelveDialog(self._repoagent, self) dlg.finished.connect(self._refreshCommitTabIfNeeded) return dlg def verify(self): cmdline = ['verify', '--verbose'] dlg = cmdui.CmdSessionDialog(self) dlg.setWindowIcon(qtlib.geticon('hg-verify')) dlg.setWindowTitle(_('%s - verify repository') % self.repo.displayname) dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) dlg.setSession(self._repoagent.runCommand(cmdline, self)) dlg.exec_() def recover(self): cmdline = ['recover', '--verbose'] dlg = cmdui.CmdSessionDialog(self) dlg.setWindowIcon(qtlib.geticon('hg-recover')) dlg.setWindowTitle(_('%s - recover repository') % self.repo.displayname) dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) dlg.setSession(self._repoagent.runCommand(cmdline, self)) dlg.exec_() def rollback(self): def read_undo(): if os.path.exists(self.repo.sjoin('undo')): try: args = self.repo.opener('undo.desc', 'r').read().splitlines() return args[1], int(args[0]) except (IOError, IndexError, ValueError): pass return None data = read_undo() if data is None: InfoMsgBox(_('No transaction available'), _('There is no rollback transaction available')) return elif data[0] == 'commit': if not QuestionMsgBox(_('Undo last commit?'), _('Undo most recent commit (%d), preserving file changes?') % data[1]): return else: if not QuestionMsgBox(_('Undo last transaction?'), _('Rollback to revision %d (undo %s)?') % (data[1]-1, data[0])): return try: rev = self.repo['.'].rev() except Exception, e: InfoMsgBox(_('Repository Error'), _('Unable to determine working copy revision\n') + hglib.tounicode(e)) return if rev >= data[1] and not QuestionMsgBox( _('Remove current working revision?'), _('Your current working revision (%d) will be removed ' 'by this rollback, leaving uncommitted changes.\n ' 'Continue?' % rev)): return cmdline = ['rollback', '--verbose'] sess = self._runCommand(cmdline) sess.commandFinished.connect(self._notifyWorkingDirChanges) def purge(self): dlg = purge.PurgeDialog(self._repoagent, self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.showMessage.connect(self.showMessage) dlg.progress.connect(self.progress) dlg.exec_() ## End workbench event forwards @pyqtSlot(unicode, dict) def grep(self, pattern='', opts={}): """Open grep task tab""" opts = dict((str(k), str(v)) for k, v in opts.iteritems()) self.taskTabsWidget.setCurrentIndex(self.grepTabIndex) self.grepDemand.setSearch(pattern, **opts) self.grepDemand.runSearch() def setupModels(self): # Filter revision set in case revisions were removed self.revset = [r for r in self.revset if r < len(self.repo)] self.repomodel = HgRepoListModel(self.repo, self.repoview.colselect[0], self.filterbar.branch(), self.revset, self.revsetfilter, self, self.filterbar.getShowHidden(), self.filterbar.branchAncestorsIncluded(), self.filterbar.getShowGraftSource()) self.repomodel.filled.connect(self.modelFilled) self.repomodel.loaded.connect(self.modelLoaded) self.repomodel.showMessage.connect(self.showMessage) oldmodel = self.repoview.model() self.repoview.setModel(self.repomodel) if oldmodel: oldmodel.deleteLater() try: self._last_series = self.repo.mq.series[:] except AttributeError: self._last_series = [] def modelFilled(self): 'initial batch of revisions loaded' self.repoview.goto(self._reload_rev) # emits revisionSelected self.repoview.resizeColumns() def modelLoaded(self): 'all revisions loaded (graph generator completed)' # Perhaps we can update a GUI element later, to indicate full load pass def onRevisionClicked(self, rev): 'User clicked on a repoview row' self.clearInfoBar(qtlib.InfoBar.INFO) tw = self.taskTabsWidget cw = tw.currentWidget() if not cw.canswitch(): return ctx = self.repo.changectx(rev) if rev is None or ('mq' in self.repo.extensions() and 'qtip' in ctx.tags() and self.repo['.'].rev() == rev): # Clicking on working copy or on the topmost applied patch # (_if_ it is also the working copy parent) switches to the commit tab tw.setCurrentIndex(self.commitTabIndex) else: # Clicking on a normal revision switches from commit tab tw.setCurrentIndex(self.logTabIndex) def onRevisionSelected(self, rev): 'View selection changed, could be a reload' self.showMessage('') if self.repomodel.graph is None: return try: self.revDetailsWidget.onRevisionSelected(rev) self.revisionSelected.emit(rev) if type(rev) != str: # Regular patch or working directory if self.manifestDemand.isHidden(): self.manifestDemand.forward('selectRev', rev) else: self.manifestDemand.forward('setRev', rev) self.grepDemand.forward('setRevision', rev) self.syncDemand.forward('refreshTargets', rev) self.commitDemand.forward('setRev', rev) else: # unapplied patch if self.manifestDemand.isHidden(): self.manifestDemand.forward('selectRev', None) else: self.manifestDemand.forward('setRev', None) except (IndexError, error.RevlogError, error.Abort), e: self.showMessage(hglib.tounicode(str(e))) def gotoParent(self): self.goto('.') def gotoTip(self): self.repoview.clearSelection() self.goto('tip') def goto(self, rev): self._reload_rev = rev self.repoview.goto(rev) def onRevisionActivated(self, rev): qgoto = False if isinstance(rev, basestring): qgoto = True else: ctx = self.repo.changectx(rev) if 'qparent' in ctx.tags() or ctx.thgmqappliedpatch(): qgoto = True if 'qtip' in ctx.tags(): qgoto = False if qgoto: self.qgotoSelectedRevision() else: self.visualDiffRevision() def reload(self, invalidate=True): 'Initiate a refresh of the repo model, rebuild graph' try: if invalidate: self.repo.thginvalidate() self.rebuildGraph() self.reloadTaskTab() except EnvironmentError, e: self.showMessage(hglib.tounicode(str(e))) def rebuildGraph(self): 'Called by repositoryChanged signals, and during reload' self.showMessage('') if len(self.repo) < self.repolen: # repo has been stripped, invalidate active revision sets if self.bundle: self.clearBundle() self.showMessage(_('Repository stripped, incoming preview ' 'cleared')) elif self.revset: self.revset = [] self.filterbar.setQuery('') self.repoview.enablefilterpalette(False) self.showMessage(_('Repository stripped, revision set cleared')) if not self.bundle: self.repolen = len(self.repo) self._reload_rev = self.rev if self.rev is None: pass elif type(self.rev) is str: try: if self.rev not in self.repo.mq.series: # patch is no longer in the series, find a neighbor idx = self._last_series.index(self._reload_rev) - 1 self._reload_rev = self._last_series[idx] while self._reload_rev not in self.repo.mq.series and idx: idx -= 1 self._reload_rev = self._last_series[idx] except (AttributeError, IndexError, ValueError): self._reload_rev = 'tip' elif len(self.repo) <= self.rev: self._reload_rev = 'tip' self.setupModels() self.filterbar.refresh() self.repoview.saveSettings() def reloadTaskTab(self): tti = self.taskTabsWidget.currentIndex() if tti == self.logTabIndex: ttw = self.revDetailsWidget elif tti == self.commitTabIndex: ttw = self.commitDemand.get() elif tti == self.manifestTabIndex: ttw = self.manifestDemand.get() elif tti == self.syncTabIndex: ttw = self.syncDemand.get() elif tti == self.grepTabIndex: ttw = self.grepDemand.get() elif tti == self.pbranchTabIndex: ttw = self.pbranchDemand.get() if ttw: ttw.reload() def refresh(self): 'Refresh the repo model view, clear cached data' self.repo.thginvalidate() self.repomodel.invalidate() self.revDetailsWidget.reload() self.filterbar.refresh() @pyqtSlot() def repositoryChanged(self): 'Repository has detected a changelog / dirstate change' if self.isVisible(): try: self.rebuildGraph() except (error.RevlogError, error.RepoError), e: self.showMessage(hglib.tounicode(str(e))) self.repomodel = HgRepoListModel(None, self.repoview.colselect[0], None, None, False, self) self.repoview.setModel(self.repomodel) else: self.dirty = True @pyqtSlot() def configChanged(self): 'Repository is reporting its config files have changed' self.repomodel.invalidate() self.revDetailsWidget.reload() self.titleChanged.emit(self.title()) self.updateTaskTabs() def updateTaskTabs(self): val = self.repo.ui.config('tortoisehg', 'tasktabs', 'off').lower() if val == 'east': self.taskTabsWidget.setTabPosition(QTabWidget.East) self.taskTabsWidget.tabBar().show() elif val == 'west': self.taskTabsWidget.setTabPosition(QTabWidget.West) self.taskTabsWidget.tabBar().show() else: self.taskTabsWidget.tabBar().hide() @pyqtSlot(QString, bool) def setBranch(self, branch, allparents): self.repomodel.setBranch(branch, allparents=allparents) self.titleChanged.emit(self.title()) self._resetBrowseHistoryOnFilterChange() @pyqtSlot(bool) def setShowHidden(self, showhidden): self.repomodel.setShowHidden(showhidden) self._resetBrowseHistoryOnFilterChange() @pyqtSlot(bool) def setShowGraftSource(self, showgraftsource): self.repomodel.setShowGraftSource(showgraftsource) ## ## Workbench methods ## def canGoBack(self): return self.repoview.canGoBack() def canGoForward(self): return self.repoview.canGoForward() def loadSettings(self): s = QSettings() repoid = str(self.repo[0]) self.revDetailsWidget.loadSettings(s) self.filterbar.loadSettings(s) self.repotabs_splitter.restoreState( s.value('repowidget/splitter-'+repoid).toByteArray()) QTimer.singleShot(0, lambda: self.toolbarVisibilityChanged.emit()) def okToContinue(self): if not self.commitDemand.canExit(): self.taskTabsWidget.setCurrentIndex(self.commitTabIndex) self.showMessage(_('Commit tab cannot exit')) return False if not self.syncDemand.canExit(): self.taskTabsWidget.setCurrentIndex(self.syncTabIndex) self.showMessage(_('Sync tab cannot exit')) return False if not self.grepDemand.canExit(): self.taskTabsWidget.setCurrentIndex(self.grepTabIndex) self.showMessage(_('Search tab cannot exit')) return False if self._repoagent.isBusy(): self.showMessage(_('Repository command still running')) return False return True def closeRepoWidget(self): '''returns False if close should be aborted''' if not self.okToContinue(): return False s = QSettings() if self.isVisible(): try: repoid = str(self.repo[0]) s.setValue('repowidget/splitter-'+repoid, self.repotabs_splitter.saveState()) except EnvironmentError: pass self.revDetailsWidget.saveSettings(s) self.commitDemand.forward('saveSettings', s, 'workbench') self.manifestDemand.forward('saveSettings', s, 'workbench') self.grepDemand.forward('saveSettings', s) self.filterbar.saveSettings(s) self.repoview.saveSettings(s) return True def setSyncUrl(self, url): """Change the current peer-repo url of the sync widget; url may be a symbolic name defined in [paths] section""" self.syncDemand.get().setUrl(url) def incoming(self): self.syncDemand.get().incoming() def pull(self): self.syncDemand.get().pull() def outgoing(self): self.syncDemand.get().outgoing() def push(self, confirm=None, **kwargs): """Call sync push. If confirm is False, the user will not be prompted for confirmation. If confirm is True, the prompt might be used. """ self.syncDemand.get().push(confirm, **kwargs) self.outgoingMode = False @pyqtSlot() def pushCompleted(self): if not self.clearRevisionSet(): self.reload() ## ## Repoview context menu ## def viewMenuRequest(self, point, selection): 'User requested a context menu in repo view widget' # selection is a list of the currently selected revisions. # Integers for changelog revisions, None for the working copy, # or strings for unapplied patches. if len(selection) == 0: return self.menuselection = selection if self.bundle: if len(selection) == 1: self.bundlemenu.exec_(point) return if self.outgoingMode: if len(selection) == 1: self.outgoingcmenu.exec_(point) return allunapp = False if 'mq' in self.repo.extensions(): for rev in selection: if not self.repo.changectx(rev).thgmqunappliedpatch(): break else: allunapp = True if allunapp: self.unappliedPatchMenu(point, selection) elif len(selection) == 1: self.singleSelectionMenu(point, selection) elif len(selection) == 2: self.doubleSelectionMenu(point, selection) else: self.multipleSelectionMenu(point, selection) def singleSelectionMenu(self, point, selection): ctx = self.repo.changectx(self.rev) applied = ctx.thgmqappliedpatch() working = self.rev is None tags = ctx.tags() for item in self.singlecmenuitems: enabled = item.enableFunc(applied, working, tags) item.setEnabled(enabled) self.singlecmenu.exec_(point) def doubleSelectionMenu(self, point, selection): for r in selection: # No pair menu if working directory or unapplied patch if type(r) is not int: return self.paircmenu.exec_(point) def multipleSelectionMenu(self, point, selection): for r in selection: # No multi menu if working directory or unapplied patch if type(r) is not int: return self.multicmenu.exec_(point) def unappliedPatchMenu(self, point, selection): q = self.repo.mq ispushable = False unapplied = 0 for i in xrange(q.seriesend(), len(q.series)): pushable, reason = q.pushable(i) if pushable: if unapplied == 0: qnext = q.series[i] if self.rev == q.series[i]: ispushable = True unapplied += 1 self.unappacts[0].setEnabled(ispushable and len(selection) == 1) self.unappacts[1].setEnabled(ispushable and len(selection) == 1) self.unappacts[2].setEnabled(ispushable and len(selection) == 1 and \ self.rev != qnext) self.unappacts[3].setEnabled('qtip' in self.repo.tags()) self.unappacts[4].setEnabled(True) self.unappacts[5].setEnabled(unapplied > 1) self.unappacts[6].setEnabled(len(selection) == 1) self.unappcmenu.exec_(point) def generateSingleMenu(self, mode=None): items = [] # This menu will never be opened for an unapplied patch, they # have their own menu. # # iswd = working directory # isrev = the changeset has an integer revision number # isctx = changectx or workingctx # fixed = the changeset is considered permanent # applied = an applied patch # qgoto = applied patch or qparent isrev = lambda ap, wd, tags: not wd iswd = lambda ap, wd, tags: bool(wd) isctx = lambda ap, wd, tags: True fixed = lambda ap, wd, tags: not (ap or wd) applied = lambda ap, wd, tags: ap qgoto = lambda ap, wd, tags: ('qparent' in tags) or \ (ap) exs = self.repo.extensions() def entry(menu, ext=None, func=None, desc=None, icon=None, cb=None): if ext and ext not in exs: return if desc is None: return menu.addSeparator() act = QAction(desc, self) if cb: act.triggered.connect(cb) if icon: act.setIcon(qtlib.geticon(icon)) act.enableFunc = func menu.addAction(act) items.append(act) return act menu = QMenu(self) if mode == 'outgoing': pushtypeicon = {'all': None, 'branch': None, 'revision': None} defaultpush = self.repo.ui.config( 'tortoisehg', 'defaultpush', 'all') pushtypeicon[defaultpush] = 'hg-push' submenu = menu.addMenu(_('Pus&h')) entry(submenu, None, isrev, _('Push to &Here'), pushtypeicon['revision'], self.pushToRevision) entry(submenu, None, isrev, _('Push Selected &Branch'), pushtypeicon['branch'], self.pushBranch) entry(submenu, None, isrev, _('Push &All'), pushtypeicon['all'], self.pushAll) entry(menu) entry(menu, None, isrev, _('&Update...'), 'hg-update', self.updateToRevision) entry(menu) entry(menu, None, isctx, _('&Diff to Parent'), 'visualdiff', self.visualDiffRevision) entry(menu, None, isrev, _('Diff to &Local'), 'ldiff', self.visualDiffToLocal) entry(menu, None, isctx, _('Bro&wse at Revision'), 'hg-annotate', self.manifestRevision) entry(menu, None, isrev, _('&Similar Revisions...'), 'view-filter', self.matchRevision) entry(menu) entry(menu, None, fixed, _('&Merge with Local...'), 'hg-merge', self.mergeWithRevision) entry(menu) entry(menu, None, fixed, _('&Tag...'), 'hg-tag', self.tagToRevision) entry(menu, None, fixed, _('Boo&kmark...'), 'hg-bookmarks', self.bookmarkRevision) entry(menu, 'gpg', fixed, _('Sig&n...'), 'hg-sign', self.signRevision) entry(menu) entry(menu, None, fixed, _('&Backout...'), 'hg-revert', self.backoutToRevision) entry(menu) entry(menu, None, isrev, _('Copy &Hash'), 'copy-hash', self.copyHash) entry(menu) submenu = menu.addMenu(_('E&xport')) entry(submenu, None, isrev, _('E&xport Patch...'), 'hg-export', self.exportRevisions) entry(submenu, None, isrev, _('&Email Patch...'), 'mail-forward', self.emailSelectedRevisions) entry(submenu, None, isrev, _('&Archive...'), 'hg-archive', self.archiveRevision) entry(submenu, None, isrev, _('&Bundle Rev and Descendants...'), 'hg-bundle', self.bundleRevisions) entry(submenu, None, isctx, _('&Copy Patch'), 'copy-patch', self.copyPatch) entry(menu) submenu = menu.addMenu(_('Change &Phase to')) submenu.triggered.connect(self._changePhaseByMenu) for pnum, pname in enumerate(phases.phasenames): entry(submenu, None, isrev, pname).setData(pnum) entry(menu) entry(menu, None, isrev, _('&Graft to Local...'), 'hg-transplant', self.graftRevisions) if 'mq' in exs or 'rebase' in exs or 'strip' in exs: submenu = menu.addMenu(_('Modi&fy History')) entry(submenu, 'mq', applied, _('&Unapply Patch'), 'hg-qgoto', self.qgotoParentRevision) entry(submenu, 'mq', fixed, _('Import to &MQ'), 'qimport', self.qimportRevision) entry(submenu, 'mq', applied, _('&Finish Patch'), 'qfinish', self.qfinishRevision) entry(submenu, 'mq', applied, _('Re&name Patch...'), None, self.qrename) entry(submenu, 'mq') if self._mqActions: entry(submenu, 'mq', isctx, _('MQ &Options'), None, self._mqActions.launchOptionsDialog) entry(submenu, 'mq') entry(submenu, 'rebase', isrev, _('&Rebase...'), 'hg-rebase', self.rebaseRevision) entry(submenu, 'rebase') if 'mq' in exs or 'strip' in exs: entry(submenu, None, fixed, _('&Strip...'), 'menudelete', self.stripRevision) entry(menu, 'reviewboard', isrev, _('Post to Re&view Board...'), 'reviewboard', self.sendToReviewBoard) entry(menu, 'rupdate', fixed, _('&Remote Update...'), 'hg-update', self.rupdate) def _setupCustomSubmenu(menu): tools, toollist = hglib.tortoisehgtools(self.repo.ui, selectedlocation='workbench.revdetails.custom-menu') if not tools: return istrue = lambda ap, wd, tags: True enablefuncs = { 'istrue': istrue, 'iswd': iswd, 'isrev': isrev, 'isctx': isctx, 'fixed': fixed, 'applied': applied, 'qgoto': qgoto } entry(menu) submenu = menu.addMenu(_('Custom Tools')) submenu.triggered.connect(self._runCustomCommandByMenu) for name in toollist: if name == '|': entry(submenu) continue info = tools.get(name, None) if info is None: continue command = info.get('command', None) if not command: continue workingdir = info.get('workingdir', '') showoutput = info.get('showoutput', False) label = info.get('label', name) icon = info.get('icon', 'tools-spanner-hammer') enable = info.get('enable', 'istrue').lower() if enable in enablefuncs: enable = enablefuncs[enable] else: continue a = entry(submenu, None, enable, label, icon) a.setData((command, showoutput, workingdir)) _setupCustomSubmenu(menu) if mode == 'outgoing': self.outgoingcmenu = menu self.outgoingcmenuitems = items else: self.singlecmenu = menu self.singlecmenuitems = items def _gotoAncestor(self): ancestor = self.repo[self.menuselection[0]] for rev in self.menuselection[1:]: ctx = self.repo[rev] ancestor = ancestor.ancestor(ctx) self.goto(ancestor.rev()) def generatePairMenu(self): def dagrange(): revA, revB = self.menuselection if revA > revB: B, A = self.menuselection else: A, B = self.menuselection func = revset.match(self.repo.ui, '%s::%s' % (A, B)) return [c for c in func(self.repo, range(len(self.repo)))] def exportPair(): self.exportRevisions(self.menuselection) def exportDiff(): root = self.repo.root filename = '%s_%d_to_%d.diff' % (os.path.basename(root), self.menuselection[0], self.menuselection[1]) file = QFileDialog.getSaveFileName(self, _('Write diff file'), hglib.tounicode(os.path.join(root, filename))) if not file: return diff = self._buildPatch('diff') try: f = open(file, "wb") try: f.write(diff) finally: f.close() except Exception, e: WarningMsgBox(_('Repository Error'), _('Unable to write diff file')) def exportDagRange(): l = dagrange() if l: self.exportRevisions(l) def diffPair(): revA, revB = self.menuselection dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], {'rev':(str(revA), str(revB))}) if dlg: dlg.exec_() def emailPair(): self._emailRevisions(self.menuselection) def emailDagRange(): l = dagrange() if l: self._emailRevisions(l) def bundleDagRange(): l = dagrange() if l: self.bundleRevisions(base=l[0], tip=l[-1]) def bisectNormal(): revA, revB = self.menuselection opts = {'good':str(revA), 'bad':str(revB)} dlg = bisect.BisectDialog(self._repoagent, opts, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def bisectReverse(): revA, revB = self.menuselection opts = {'good':str(revB), 'bad':str(revA)} dlg = bisect.BisectDialog(self._repoagent, opts, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def compressDlg(): ctxa, ctxb = map(self.repo.hgchangectx, self.menuselection) if ctxa.ancestor(ctxb) == ctxb: revs = self.menuselection[:] elif ctxa.ancestor(ctxb) == ctxa: revs = [self.menuselection[1], self.menuselection[0]] else: InfoMsgBox(_('Unable to compress history'), _('Selected changeset pair not related')) return dlg = compress.CompressDialog(self._repoagent, revs, self) dlg.exec_() def rebaseDlg(): opts = {'source': self.menuselection[0], 'dest': self.menuselection[1]} dlg = rebase.RebaseDialog(self._repoagent, self, **opts) dlg.exec_() exs = self.repo.extensions() menu = QMenu(self) for name, cb, icon, ext in ( (_('Visual Diff...'), diffPair, 'visualdiff', None), (_('Export Diff...'), exportDiff, 'hg-export', None), (None, None, None, None), (_('Export Selected...'), exportPair, 'hg-export', None), (_('Email Selected...'), emailPair, 'mail-forward', None), (_('Copy Selected as Patch'), self.copyPatch, 'copy-patch', None), (None, None, None, None), (_('Export DAG Range...'), exportDagRange, 'hg-export', None), (_('Email DAG Range...'), emailDagRange, 'mail-forward', None), (_('Bundle DAG Range...'), bundleDagRange, 'hg-bundle', None), (None, None, None, None), (_('Bisect - Good, Bad...'), bisectNormal, 'hg-bisect-good-bad', None), (_('Bisect - Bad, Good...'), bisectReverse, 'hg-bisect-bad-good', None), (_('Compress History...'), compressDlg, 'hg-compress', None), (_('Rebase...'), rebaseDlg, 'hg-rebase', 'rebase'), (None, None, None, None), (_('Goto common ancestor'), self._gotoAncestor, 'hg-merge', None), (_('Similar revisions...'), self.matchRevision, 'view-filter', None), (None, None, None, None), (_('Graft Selected to local...'), self.graftRevisions, 'hg-transplant', None), ): if name is None: menu.addSeparator() continue if ext and ext not in exs: continue a = QAction(name, self) if icon: a.setIcon(qtlib.geticon(icon)) a.triggered.connect(cb) menu.addAction(a) if 'reviewboard' in self.repo.extensions(): menu.addSeparator() a = QAction(_('Post Selected to Review Board...'), self) a.triggered.connect(self.sendToReviewBoard) menu.addAction(a) self.paircmenu = menu def generateUnappliedPatchMenu(self): def qdeleteact(): """Delete unapplied patch(es)""" patches = map(hglib.tounicode, self.menuselection) self._mqActions.deletePatches(patches) def qfoldact(): patches = map(hglib.tounicode, self.menuselection) self._mqActions.foldPatches(patches) menu = QMenu(self) acts = [] for name, cb, icon in ( (_('Apply patch'), self.qpushRevision, 'hg-qpush'), (_('Apply onto original parent'), self.qpushExactRevision, None), (_('Apply only this patch'), self.qpushMoveRevision, None), (_('Fold patches...'), qfoldact, 'hg-qfold'), (_('Delete patches...'), qdeleteact, 'hg-qdelete'), (_('Rename patch...'), self.qrename, None)): act = QAction(name, self) act.triggered.connect(cb) if icon: act.setIcon(qtlib.geticon(icon)) acts.append(act) menu.addAction(act) menu.addSeparator() acts.append(menu.addAction(_('MQ &Options'), self._mqActions.launchOptionsDialog)) self.unappcmenu = menu self.unappacts = acts def generateMultipleSelectionMenu(self): def exportSel(): self.exportRevisions(self.menuselection) def emailSel(): self._emailRevisions(self.menuselection) menu = QMenu(self) for name, cb, icon in ( (_('Export Selected...'), exportSel, 'hg-export'), (_('Email Selected...'), emailSel, 'mail-forward'), (_('Copy Selected as Patch'), self.copyPatch, 'copy-patch'), (None, None, None), (_('Goto common ancestor'), self._gotoAncestor, 'hg-merge'), (_('Similar revisions...'), self.matchRevision, 'view-filter'), (None, None, None), (_('Graft Selected to local...'), self.graftRevisions, 'hg-transplant'), ): if name is None: menu.addSeparator() continue a = QAction(name, self) if icon: a.setIcon(qtlib.geticon(icon)) a.triggered.connect(cb) menu.addAction(a) if 'reviewboard' in self.repo.extensions(): a = QAction(_('Post Selected to Review Board...'), self) a.triggered.connect(self.sendToReviewBoard) menu.addAction(a) self.multicmenu = menu def generateBundleMenu(self): menu = QMenu(self) for name, cb, icon in ( (_('Pull to here...'), self.pullBundleToRev, 'hg-pull-to-here'), (_('Visual diff...'), self.visualDiffRevision, 'visualdiff'), ): a = QAction(name, self) a.triggered.connect(cb) if icon: a.setIcon(qtlib.geticon(icon)) menu.addAction(a) self.bundlemenu = menu def generateOutgoingMenu(self): self.generateSingleMenu(mode='outgoing') def exportRevisions(self, revisions): if not revisions: revisions = [self.rev] if len(revisions) == 1: if isinstance(self.rev, int): defaultpath = os.path.join(self.repoRootPath(), '%d.patch' % self.rev) else: defaultpath = self.repoRootPath() ret = QFileDialog.getSaveFileName(self, _('Export patch'), defaultpath, _('Patch Files (*.patch)')) if not ret: return epath = unicode(ret) udir = os.path.dirname(epath) custompath = True else: udir = QFileDialog.getExistingDirectory(self, _('Export patch'), hglib.tounicode(self.repo.root)) if not udir: return udir = unicode(udir) epath = os.path.join(udir, self.repo.shortname + '_%r.patch') custompath = False cmdline = ['export', '--verbose', '--output', epath] existingRevisions = [] for rev in revisions: if custompath: path = epath else: path = epath % rev if os.path.exists(path): if os.path.isfile(path): existingRevisions.append(rev) else: QMessageBox.warning(self, _('Cannot export revision'), (_('Cannot export revision %s into the file named:' '\n\n%s\n') % (rev, epath % rev)) + \ _('There is already an existing folder ' 'with that same name.')) return cmdline.extend(['--rev', str(rev)]) if existingRevisions: buttonNames = [_("Replace"), _("Append"), _("Abort")] warningMessage = \ _('There are existing patch files for %d revisions (%s) ' 'in the selected location (%s).\n\n') \ % (len(existingRevisions), " ,".join([str(rev) for rev in existingRevisions]), udir) warningMessage += \ _('What do you want to do?\n') + u'\n' + \ u'- ' + _('Replace the existing patch files.\n') + \ u'- ' + _('Append the changes to the existing patch files.\n') + \ u'- ' + _('Abort the export operation.\n') res = qtlib.CustomPrompt(_('Patch files already exist'), warningMessage, self, buttonNames, 0, 2).run() if buttonNames[res] == _("Replace"): # Remove the existing patch files for rev in existingRevisions: if custompath: os.remove(epath) else: os.remove(epath % rev) elif buttonNames[res] == _("Abort"): return self._runCommand(cmdline) if len(revisions) == 1: # Show a message box with a link to the export folder and to the # exported file rev = revisions[0] patchfilename = os.path.normpath(epath) patchdirname = os.path.normpath(os.path.dirname(epath)) patchshortname = os.path.basename(patchfilename) if patchdirname.endswith(os.path.sep): patchdirname = patchdirname[:-1] qtlib.InfoMsgBox(_('Patch exported'), _('Revision #%d (%s) was exported to:

    ' '%s%s' '%s') \ % (rev, str(self.repo[rev]), patchdirname, patchdirname, os.path.sep, patchfilename, patchshortname)) else: # Show a message box with a link to the export folder qtlib.InfoMsgBox(_('Patches exported'), _('%d patches were exported to:

    ' '%s') \ % (len(revisions), udir, udir)) def visualDiffRevision(self): opts = dict(change=self.rev) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() def visualDiffToLocal(self): if self.rev is None: return opts = dict(rev=['rev(%d)' % self.rev]) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [], opts) if dlg: dlg.exec_() @pyqtSlot() def updateToRevision(self): rev = hglib.getrevisionlabel(self.repo, self.rev) dlg = update.UpdateDialog(self._repoagent, rev, self) dlg.output.connect(self.output) dlg.makeLogVisible.connect(self.makeLogVisible) dlg.progress.connect(self.progress) dlg.exec_() def matchRevision(self): revlist = self.rev if len(self.menuselection) > 1: revlist = '|'.join([str(rev) for rev in self.menuselection]) dlg = matching.MatchDialog(self.repo, revlist, self) if dlg.exec_(): self.setFilter(dlg.revsetexpression) def pushAll(self): self.syncDemand.forward('push', False, pushall=True) def pushToRevision(self): # Do not ask for confirmation self.syncDemand.forward('push', False, rev=self.rev) def pushBranch(self): # Do not ask for confirmation self.syncDemand.forward('push', False, branch=self.repo[self.rev].branch()) def manifestRevision(self): if QApplication.keyboardModifiers() & Qt.ShiftModifier: self._dialogs.openNew(RepoWidget._createManifestDialog) else: dlg = self._dialogs.open(RepoWidget._createManifestDialog) dlg.setRev(self.rev) def _createManifestDialog(self): return ManifestDialog(self._repoagent, self.rev) def mergeWithRevision(self): pctx = self.repo['.'] octx = self.repo[self.rev] if pctx == octx: QMessageBox.warning(self, _('Unable to merge'), _('You cannot merge a revision with itself')) return self._dialogs.open(RepoWidget._createMergeDialog, self.rev) def _createMergeDialog(self, rev): return merge.MergeDialog(self._repoagent, rev, self) def tagToRevision(self): dlg = tag.TagDialog(self._repoagent, rev=str(self.rev), parent=self) dlg.exec_() def bookmarkRevision(self): dlg = bookmark.BookmarkDialog(self._repoagent, self.rev, self) dlg.exec_() def signRevision(self): dlg = sign.SignDialog(self._repoagent, self.rev, self) dlg.exec_() def graftRevisions(self): """Graft selected revision on top of working directory parent""" revlist = [] for rev in sorted(self.repoview.selectedRevisions()): revlist.append(str(rev)) if not revlist: revlist = [self.rev] dlg = graft.GraftDialog(self._repoagent, self, source=revlist) if dlg.valid: dlg.exec_() def backoutToRevision(self): dlg = backout.BackoutDialog(self._repoagent, self.rev, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() def stripRevision(self): 'Strip the selected revision and all descendants' dlg = thgstrip.StripDialog(self._repoagent, rev=str(self.rev), parent=self) dlg.showBusyIcon.connect(self.onShowBusyIcon) dlg.hideBusyIcon.connect(self.onHideBusyIcon) dlg.exec_() def sendToReviewBoard(self): self._dialogs.open(RepoWidget._createPostReviewDialog, tuple(self.repoview.selectedRevisions())) def _createPostReviewDialog(self, revs): return postreview.PostReviewDialog(self.repo.ui, self._repoagent, revs) def rupdate(self): import rupdate dlg = rupdate.rUpdateDialog(self._repoagent, self.rev, self) dlg.output.connect(self.output) dlg.makeLogVisible.connect(self.makeLogVisible) dlg.progress.connect(self.progress) dlg.exec_() @pyqtSlot() def emailSelectedRevisions(self): self._emailRevisions(self.repoview.selectedRevisions()) def _emailRevisions(self, revs): self._dialogs.open(RepoWidget._createEmailDialog, tuple(revs)) def _createEmailDialog(self, revs): return hgemail.EmailDialog(self._repoagent, revs) def archiveRevision(self): rev = hglib.getrevisionlabel(self.repo, self.rev) dlg = archive.ArchiveDialog(self._repoagent, rev, self) dlg.makeLogVisible.connect(self.makeLogVisible) dlg.output.connect(self.output) dlg.progress.connect(self.progress) dlg.exec_() def bundleRevisions(self, base=None, tip=None): root = self.repoRootPath() if base is None or base is False: base = self.rev data = dict(name=os.path.basename(root), base=base) if tip is None: filename = '%(name)s_%(base)s_and_descendants.hg' % data else: data.update(rev=tip) filename = '%(name)s_%(base)s_to_%(rev)s.hg' % data file = QFileDialog.getSaveFileName(self, _('Write bundle'), os.path.join(root, filename)) if not file: return cmdline = ['bundle', '--verbose'] parents = [r.rev() == -1 and 'null' or str(r.rev()) for r in self.repo[base].parents()] for p in parents: cmdline.extend(['--base', p]) if tip: cmdline.extend(['--rev', str(tip)]) else: cmdline.extend(['--rev', 'heads(descendants(%s))' % base]) cmdline.append(unicode(file)) self._runCommand(cmdline) def _buildPatch(self, command=None): if not command: # workingdir revision cannot be exported command = self.rev and 'export' or 'diff' assert command in ('export', 'diff') from mercurial import commands _ui = self.repo.ui _ui.pushbuffer() try: if command == 'export': # patches should be in chronological order revs = sorted(self.menuselection) commands.export(_ui, self.repo, rev=revs, output='') else: revs = self.rev and self.menuselection or None commands.diff(_ui, self.repo, rev=revs) except NameError: raise except Exception, e: _ui.popbuffer() self.showMessage(hglib.tounicode(str(e))) if 'THGDEBUG' in os.environ: import traceback traceback.print_exc() return return _ui.popbuffer() @pyqtSlot() def copyPatch(self): output = self._buildPatch() if output: mdata = QMimeData() mdata.setData('text/x-diff', output) # for lossless import mdata.setText(hglib.tounicode(output)) QApplication.clipboard().setMimeData(mdata) def copyHash(self): clip = QApplication.clipboard() clip.setText(binascii.hexlify(self.repo[self.rev].node())) def changePhase(self, phase): currentphase = self.repo[self.rev].phase() if currentphase == phase: # There is nothing to do, we are already in the target phase return phasestr = phases.phasenames[phase] cmdline = ['phase', '--rev', '%s' % self.rev, '--%s' % phasestr] if currentphase < phase: # Ask the user if he wants to force the transition title = _('Backwards phase change requested') if currentphase == phases.draft and phase == phases.secret: # Here we are sure that the current phase is draft and the target phase is secret # Nevertheless we will not hard-code those phase names on the dialog strings to # make sure that the proper phase name translations are used main = _('Do you really want to make this revision secret?') text = _('Making a "draft" revision "secret" ' 'is generally a safe operation.\n\n' 'However, there are a few caveats:\n\n' '- "secret" revisions are not pushed. ' 'This can cause you trouble if you\n' 'refer to a secret subrepo revision.\n\n' '- If you pulled this revision from ' 'a non publishing server it may be\n' 'moved back to "draft" if you pull ' 'again from that particular server.\n\n' 'Please be careful!') labels = ((QMessageBox.Yes, _('&Make secret')), (QMessageBox.No, _('&Cancel'))) else: main = _('Do you really want to force a backwards phase transition?') text = _('You are trying to move the phase of revision %d backwards,\n' 'from "%s" to "%s".\n\n' 'However, "%s" is a lower phase level than "%s".\n\n' 'Moving the phase backwards is not recommended.\n' 'For example, it may result in having multiple heads\nif you ' 'modify a revision that you have already pushed\nto a server.\n\n' 'Please be careful!') % (self.rev, phases.phasenames[currentphase], phasestr, phasestr, phases.phasenames[currentphase]) labels = ((QMessageBox.Yes, _('&Force')), (QMessageBox.No, _('&Cancel'))) if not qtlib.QuestionMsgBox(title, main, text, labels=labels, parent=self): return cmdline.append('--force') self._runCommand(cmdline) @pyqtSlot(QAction) def _changePhaseByMenu(self, action): phasenum, _ok = action.data().toInt() self.changePhase(phasenum) def rebaseRevision(self): """Rebase selected revision on top of working directory parent""" opts = {'source' : self.rev, 'dest': self.repo['.'].rev()} dlg = rebase.RebaseDialog(self._repoagent, self, **opts) dlg.exec_() def qimportRevision(self): """QImport revision and all descendents to MQ""" if 'qparent' in self.repo.tags(): endrev = 'qparent' else: endrev = '' # Check whether there are existing patches in the MQ queue whose name # collides with the revisions that are going to be imported func = revset.match(self.repo.ui, '%s::%s and not hidden()' % (self.rev, endrev)) revList = [c for c in func(self.repo, range(len(self.repo)))] if endrev and not revList: # There is a qparent but the revision list is empty # This means that the qparent is not a descendant of the # selected revision QMessageBox.warning(self, _('Cannot import selected revision'), _('The selected revision (rev #%d) cannot be imported ' 'because it is not a descendant of ''qparent'' (rev #%d)') \ % (self.rev, self.repo['qparent'].rev())) return patchdir = self.repo.join('patches') def patchExists(p): return os.path.exists(os.path.join(patchdir, p)) # Note that the following two arrays are both ordered by "rev" defaultPatchNames = ['%d.diff' % rev for rev in revList] defaultPatchesExist = [patchExists(p) for p in defaultPatchNames] if any(defaultPatchesExist): # We will qimport each revision one by one, starting from the newest # To do so, we will find a valid and unique patch name for each # revision that we must qimport (i.e. a filename that does not # already exist) # and then we will import them one by one starting from the newest # one, using these unique names def getUniquePatchName(baseName): maxRetries = 99 for n in range(1, maxRetries): patchName = baseName + '_%02d.diff' % n if not patchExists(patchName): return patchName return baseName patchNames = {} for n, rev in enumerate(revList): if defaultPatchesExist[n]: patchNames[rev] = getUniquePatchName(str(rev)) else: # The default name is safe patchNames[rev] = defaultPatchNames[n] # qimport each revision individually, starting from the topmost one revList.reverse() cmdlines = [] for rev in revList: cmdlines.append(['qimport', '--rev', '%s' % rev, '--name', patchNames[rev]]) self._runCommandSequence(cmdlines) else: # There were no collisions with existing patch names, we can # simply qimport the whole revision set in a single go cmdline = ['qimport', '--rev', '%s::%s' % (self.rev, endrev)] self._runCommand(cmdline) def qfinishRevision(self): """Finish applied patches up to and including selected revision""" cmdline = ['qfinish', 'qbase::%s' % self.rev] self._runCommand(cmdline) @pyqtSlot() def qgotoParentRevision(self): """Apply an unapplied patch, or qgoto the parent of an applied patch""" self.qgotoRevision(self.repo[self.rev].p1().rev()) @pyqtSlot() def qgotoSelectedRevision(self): self.qgotoRevision(self.rev) def qgotoRevision(self, rev): """Make REV the top applied patch""" mqw = self._mqActions ctx = self.repo.changectx(rev) if 'qparent'in ctx.tags(): mqw.popAllPatches() else: mqw.gotoPatch(hglib.tounicode(ctx.thgmqpatchname())) def qrename(self): sel = self.menuselection[0] if not isinstance(sel, str): sel = self.repo.changectx(sel).thgmqpatchname() self._mqActions.renamePatch(hglib.tounicode(sel)) def _qpushRevision(self, move=False, exact=False): """QPush REV with the selected options""" ctx = self.repo.changectx(self.rev) patchname = hglib.tounicode(ctx.thgmqpatchname()) self._mqActions.pushPatch(patchname, move=move, exact=exact) def qpushRevision(self): """Call qpush with no options""" self._qpushRevision(move=False, exact=False) def qpushExactRevision(self): """Call qpush using the exact flag""" self._qpushRevision(exact=True) def qpushMoveRevision(self): """Make REV the top applied patch""" self._qpushRevision(move=True) def runCustomCommand(self, command, showoutput=False, workingdir='', files=None): """Execute 'custom commands', on the selected repository""" # Perform variable expansion # This is done in two steps: # 1. Expand environment variables command = os.path.expandvars(command).strip() if not command: InfoMsgBox(_('Invalid command'), _('The selected command is empty')) return if workingdir: workingdir = os.path.expandvars(workingdir).strip() # 2. Expand internal workbench variables def filelist2str(filelist): return ' '.join(util.shellquote( os.path.normpath(self.repo.wjoin(filename))) for filename in filelist) if files is None: files = [] vars = { 'ROOT': self.repo.root, 'REVID': str(self.repo[self.rev]), 'REV': self.rev, 'FILES': filelist2str(self.repo[self.rev].files()), 'ALLFILES': filelist2str(self.repo[self.rev]), 'SELECTEDFILES': filelist2str(files), } for var in vars: command = command.replace('{%s}' % var, str(vars[var])) if workingdir: workingdir = workingdir.replace('{%s}' % var, str(vars[var])) if not workingdir: workingdir = self.repo.root # Show the Output Log if configured to do so if showoutput: self.makeLogVisible.emit(True) # If the user wants to run mercurial, # do so via our usual runCommand method cmd = shlex.split(command) cmdtype = cmd[0].lower() if cmdtype == 'hg': sess = self._runCommand(map(hglib.tounicode, cmd[1:])) sess.commandFinished.connect(self._notifyWorkingDirChanges) return elif cmdtype == 'thg': cmd = cmd[1:] if '--repository' in cmd: _ui = ui.ui() else: cmd += ['--repository', self.repo.root] _ui = self.repo.ui.copy() _ui.ferr = cStringIO.StringIO() # avoid circular import of hgqt.run by importing it inplace from tortoisehg.hgqt import run res = run.dispatch(cmd, u=_ui) if res: errormsg = _ui.ferr.getvalue().strip() if errormsg: errormsg = \ _('The following error message was returned:' '\n\n%s') % hglib.tounicode(errormsg) errormsg +=\ _('\n\nPlease check that the "thg" command is valid.') qtlib.ErrorMsgBox( _('Failed to execute custom TortoiseHg command'), _('The command "%s" failed (code %d).') % (hglib.tounicode(command), res), errormsg) return res # Otherwise, run the selected command in the background try: res = subprocess.Popen(command, cwd=workingdir) except OSError, ex: res = 1 qtlib.ErrorMsgBox(_('Failed to execute custom command'), _('The command "%s" could not be executed.') % hglib.tounicode(command), _('The following error message was returned:\n\n"%s"\n\n' 'Please check that the command path is valid and ' 'that it is a valid application') % hglib.tounicode(ex.strerror)) return res @pyqtSlot(QAction) def _runCustomCommandByMenu(self, action): command, showoutput, workingdir = action.data().toPyObject() self.runCustomCommand(command, showoutput, workingdir) @pyqtSlot(str, list) def handleRunCustomCommandRequest(self, toolname, files): tools, toollist = hglib.tortoisehgtools(self.repo.ui) if not tools or toolname not in toollist: return toolname = str(toolname) command = tools[toolname].get('command', '') showoutput = tools[toolname].get('showoutput', False) workingdir = tools[toolname].get('workingdir', '') self.runCustomCommand(command, showoutput, workingdir, files) def _runCommand(self, cmdline): sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._makeLogVisibleOnError) sess.outputReceived.connect(self._showOutputOnInfoBar) return sess def _runCommandSequence(self, cmdlines): sess = self._repoagent.runCommandSequence(cmdlines, self) sess.commandFinished.connect(self._makeLogVisibleOnError) sess.outputReceived.connect(self._showOutputOnInfoBar) return sess @pyqtSlot(int) def _makeLogVisibleOnError(self, ret): if ret != 0: self.makeLogVisible.emit(True) @pyqtSlot() def _notifyWorkingDirChanges(self): shlib.shell_notify([self.repo.root]) @pyqtSlot() def _refreshCommitTabIfNeeded(self): """Refresh the Commit tab if the user settings require it""" if self.taskTabsWidget.currentIndex() != self.commitTabIndex: return refreshwd = self.repo.ui.config('tortoisehg', 'refreshwdstatus', 'auto') # Valid refreshwd values are 'auto', 'always' and 'alwayslocal' if refreshwd != 'auto': if refreshwd == 'always' \ or not paths.netdrive_status(self.repo.root): self.commitDemand.forward('refreshWctx') tortoisehg-2.10/tortoisehg/hgqt/thgstrip.py0000644000076400007640000002167712231647662020267 0ustar stevesteve# thgstrip.py - MQ strip dialog for TortoiseHg # # Copyright 2009 Yuki KODAMA # Copyright 2010 David Wilhelm # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _, ngettext from tortoisehg.hgqt import cmdui, cslist, qtlib class StripDialog(QDialog): """Dialog to strip changesets""" showBusyIcon = pyqtSignal(QString) hideBusyIcon = pyqtSignal(QString) def __init__(self, repoagent, rev=None, parent=None, opts={}): super(StripDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowIcon(qtlib.geticon('menudelete')) self._repoagent = repoagent # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid self.grid = grid = QGridLayout() grid.setSpacing(6) box.addLayout(grid) ### target revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) grid.addWidget(QLabel(_('Strip:')), 0, 0) grid.addWidget(combo, 0, 1) grid.addWidget(QLabel(_('Preview:')), 1, 0, Qt.AlignLeft | Qt.AlignTop) self.status = QLabel("") grid.addWidget(self.status, 1, 1, Qt.AlignLeft | Qt.AlignTop) if rev is None: rev = self.repo.dirstate.branch() else: rev = str(rev) combo.addItem(hglib.tounicode(rev)) combo.setCurrentIndex(0) for name in self.repo.namedbranches: combo.addItem(name) tags = list(self.repo.tags()) tags.sort(reverse=True) for tag in tags: combo.addItem(hglib.tounicode(tag)) ### preview box, contained in scroll area, contains preview grid self.cslist = cslist.ChangesetList(self.repo) cslistrow = 2 cslistcol = 1 grid.addWidget(self.cslist, cslistrow, cslistcol) ### options optbox = QVBoxLayout() optbox.setSpacing(6) expander = qtlib.ExpanderLabel(_('Options:'), False) expander.expanded.connect(self.show_options) grid.addWidget(expander, 3, 0, Qt.AlignLeft | Qt.AlignTop) grid.addLayout(optbox, 3, 1) self.discard_chk = QCheckBox(_('Discard local changes, no backup ' '(-f/--force)')) self.nobackup_chk = QCheckBox(_('No backup (-n/--nobackup)')) optbox.addWidget(self.discard_chk) optbox.addWidget(self.nobackup_chk) self.discard_chk.setChecked(bool(opts.get('force'))) self.nobackup_chk.setChecked(bool(opts.get('nobackup'))) ## command widget self.cmd = cmdui.Widget(True, True, self) self.cmd.commandStarted.connect(self.command_started) self.cmd.commandFinished.connect(self.command_finished) self.cmd.commandCanceling.connect(self.command_canceling) box.addWidget(self.cmd) ## bottom buttons buttons = QDialogButtonBox() self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel) self.cancel_btn.clicked.connect(self.cancel_clicked) self.close_btn = buttons.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.reject) self.close_btn.setAutoDefault(False) self.strip_btn = buttons.addButton(_('&Strip'), QDialogButtonBox.ActionRole) self.strip_btn.clicked.connect(self.strip) self.detail_btn = buttons.addButton(_('Detail'), QDialogButtonBox.ResetRole) self.detail_btn.setAutoDefault(False) self.detail_btn.setCheckable(True) self.detail_btn.toggled.connect(self.detail_toggled) grid.setRowStretch(cslistrow, 1) grid.setColumnStretch(cslistcol, 1) box.addWidget(buttons) # signal handlers self.rev_combo.editTextChanged.connect(self.preview) self.rev_combo.lineEdit().returnPressed.connect(self.strip) self.discard_chk.toggled.connect(self.preview) # dialog setting self.setLayout(box) self.layout().setSizeConstraint(QLayout.SetMinAndMaxSize) self.setWindowTitle(_('Strip - %s') % self.repo.displayname) #self.setWindowIcon(qtlib.geticon('strip')) # prepare to show self.rev_combo.lineEdit().selectAll() self.cslist.setHidden(False) self.cmd.setHidden(True) self.cancel_btn.setHidden(True) self.detail_btn.setHidden(True) self.nobackup_chk.setHidden(True) self.preview() ### Private Methods ### @property def repo(self): return self._repoagent.rawRepo() def get_rev(self): """Return the integer revision number of the input or None""" revstr = hglib.fromunicode(self.rev_combo.currentText()) if not revstr: return None try: rev = self.repo[revstr].rev() except (error.RepoError, error.LookupError): return None return rev def updatecslist(self, uselimit=True): """Update the cs list and return the success status as a bool""" rev = self.get_rev() if rev is None: return False striprevs = list(self.repo.changelog.descendants([rev])) striprevs.append(rev) striprevs.sort() self.cslist.clear() self.cslist.update(striprevs) return True @pyqtSlot() def preview(self): if self.updatecslist(): striprevs = self.cslist.curitems cstext = ngettext( "%d changeset will be stripped", "%d changesets will be stripped", len(striprevs)) % len(striprevs) self.status.setText(cstext) self.strip_btn.setEnabled(True) else: self.cslist.clear() self.cslist.updatestatus() cstext = qtlib.markup(_('Unknown revision!'), fg='red', weight='bold') self.status.setText(cstext) self.strip_btn.setDisabled(True) def strip(self): # Note that we have discussed that --hidden should only be passed to # mercurial commands when hidden revisions are shown. # However in the case of strip we can always pass it --hidden safely, # since strip will always strip all the descendants of a revision. # Thus in this case --hidden will just let us choose a hidden revision # as the base revision to strip. cmdline = ['strip', '--repository', self.repo.root, '--verbose', '--hidden'] rev = hglib.fromunicode(self.rev_combo.currentText()) if not rev: return cmdline.append(rev) if self.discard_chk.isChecked(): cmdline.append('--force') else: def isclean(): """return whether WD is changed""" wc = self.repo[None] return not (wc.modified() or wc.added() or wc.removed()) if not isclean(): main = _("Detected uncommitted local changes.") text = _("Do you want to discard them and continue?") labels = ((QMessageBox.Yes, _('&Yes (--force)')), (QMessageBox.No, _('&No'))) if qtlib.QuestionMsgBox(_('Confirm Strip'), main, text, labels=labels, parent=self): cmdline.append('--force') else: return # backup options if self.nobackup_chk.isChecked(): cmdline.append('--nobackup') # start the strip self.repo.incrementBusyCount() self.cmd.run(cmdline) ### Signal Handlers ### def cancel_clicked(self): self.cmd.cancel() self.reject() def detail_toggled(self, checked): self.cmd.setShowOutput(checked) def show_options(self, visible): self.nobackup_chk.setShown(visible) def command_started(self): self.cmd.setShown(True) self.strip_btn.setHidden(True) self.close_btn.setHidden(True) self.cancel_btn.setShown(True) self.detail_btn.setShown(True) self.showBusyIcon.emit('hg-remove') def command_finished(self, ret): self.hideBusyIcon.emit('hg-remove') self.repo.decrementBusyCount() if ret != 0 or self.cmd.outputShown(): self.detail_btn.setChecked(True) self.close_btn.setShown(True) self.close_btn.setAutoDefault(True) self.close_btn.setFocus() self.cancel_btn.setHidden(True) else: self.accept() def command_canceling(self): self.cancel_btn.setDisabled(True) tortoisehg-2.10/tortoisehg/hgqt/customtools.py0000644000076400007640000010333212212222646020771 0ustar stevesteve# customtools.py - Settings panel and configuration dialog for TortoiseHg custom tools # # This module implements 3 main classes: # # 1. A ToolsFrame which is meant to be shown on the settings dialog # 2. A ToolList widget, part of the ToolsFrame, showing a list of # configured custom tools # 3. A CustomToolConfigDialog, that can be used to add a new or # edit an existing custom tool # # The ToolsFrame and specially the ToolList must implement some methods # which are common to all settings widgets. # # Copyright 2012 Angel Ezquerra # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import re from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.util import hglib from PyQt4.QtCore import * from PyQt4.QtGui import * class ToolsFrame(QFrame): def __init__(self, ini, parent=None, **opts): QFrame.__init__(self, parent, **opts) self.widgets = [] self.ini = ini self.tortoisehgtools, guidef = hglib.tortoisehgtools(self.ini) self.setValue(self.tortoisehgtools) # The frame has a header and 3 columns: # - The header shows a combo with the list of locations # - The columns show: # - The current location tool list and its associated buttons # - The add to list button # - The "available tools" list and its associated buttons topvbox = QVBoxLayout() self.setLayout(topvbox) topvbox.addWidget(QLabel(_('Select a GUI location to edit:'))) self.locationcombo = QComboBox(self, toolTip=_('Select the toolbar or menu to change')) def selectlocation(index): location = self.locationcombo.itemData(index).toString() for widget in self.widgets: if widget.location == location: widget.removeInvalid(self.value()) widget.show() else: widget.hide() self.locationcombo.currentIndexChanged.connect(selectlocation) topvbox.addWidget(self.locationcombo) hbox = QHBoxLayout() topvbox.addLayout(hbox) vbox = QVBoxLayout() self.globaltoollist = ToolListBox(self.ini, minimumwidth=100, parent=self) self.globaltoollist.doubleClicked.connect(self.editToolItem) vbox.addWidget(QLabel(_('Tools shown on selected location'))) for location, locationdesc in hglib.tortoisehgtoollocations: self.locationcombo.addItem(locationdesc.decode('utf-8'), location) toollist = ToolListBox(self.ini, location=location, minimumwidth=100, parent=self) toollist.doubleClicked.connect(self.editToolFromName) vbox.addWidget(toollist) toollist.hide() self.widgets.append(toollist) deletefromlistbutton = QPushButton(_('Delete from list'), self) deletefromlistbutton.clicked.connect( lambda: self.forwardToCurrentToolList('deleteTool', remove=False)) vbox.addWidget(deletefromlistbutton) hbox.addLayout(vbox) vbox = QVBoxLayout() vbox.addWidget(QLabel('')) # to align all lists addtolistbutton = QPushButton('<< ' + _('Add to list') + ' <<', self) addtolistbutton.clicked.connect(self.addToList) addseparatorbutton = QPushButton('<< ' + _('Add separator'), self) addseparatorbutton.clicked.connect( lambda: self.forwardToCurrentToolList('addSeparator')) vbox.addWidget(addtolistbutton) vbox.addWidget(addseparatorbutton) vbox.addStretch() hbox.addLayout(vbox) vbox = QVBoxLayout() vbox.addWidget(QLabel(_('List of all tools'))) vbox.addWidget(self.globaltoollist) newbutton = QPushButton(_('New Tool ...'), self) newbutton.clicked.connect(self.newTool) editbutton = QPushButton(_('Edit Tool ...'), self) editbutton.clicked.connect(lambda: self.editTool(row=None)) deletebutton = QPushButton(_('Delete Tool'), self) deletebutton.clicked.connect(self.deleteCurrentTool) vbox.addWidget(newbutton) vbox.addWidget(editbutton) vbox.addWidget(deletebutton) hbox.addLayout(vbox) # Ensure that the first location list is shown selectlocation(0) def getCurrentToolList(self): index = self.locationcombo.currentIndex() location = self.locationcombo.itemData(index).toString() for widget in self.widgets: if widget.location == location: return widget return None def addToList(self): gtl = self.globaltoollist row = gtl.currentIndex().row() if row < 0: row = 0 item = gtl.item(row) if item is None: return toolname = item.text() self.forwardToCurrentToolList('addOrInsertItem', toolname, icon=item.icon()) def forwardToCurrentToolList(self, funcname, *args, **opts): w = self.getCurrentToolList() if w is not None: getattr(w, funcname)(*args, **opts) return None def newTool(self): td = CustomToolConfigDialog(self) res = td.exec_() if res: toolname, toolconfig = td.value() self.globaltoollist.addOrInsertItem( toolname, icon=toolconfig.get('icon', None)) self.tortoisehgtools[toolname] = toolconfig def editTool(self, row=None): gtl = self.globaltoollist if row is None: row = gtl.currentIndex().row() if row < 0: return self.newTool() else: item = gtl.item(row) toolname = item.text() td = CustomToolConfigDialog( self, toolname=toolname, toolconfig=self.tortoisehgtools[str(toolname)]) res = td.exec_() if res: toolname, toolconfig = td.value() icon = toolconfig.get('icon', '') if not icon: icon = td._defaulticonname item = QListWidgetItem(qtlib.geticon(icon), toolname) gtl.takeItem(row) gtl.insertItem(row, item) gtl.setCurrentRow(row) self.tortoisehgtools[toolname] = toolconfig def editToolItem(self, item): self.editTool(item.row()) def editToolFromName(self, name): # [TODO] connect to toollist doubleClick (not global) gtl = self.globaltoollist if name == gtl.SEPARATOR: return guidef = gtl.values() for row, toolname in enumerate(guidef): if toolname == name: self.editTool(row) return def deleteCurrentTool(self): row = self.globaltoollist.currentIndex().row() if row >= 0: item = self.globaltoollist.item(row) itemtext = str(item.text()) self.globaltoollist.deleteTool(row=row) self.deleteTool(itemtext) self.forwardToCurrentToolList('removeInvalid', self.value()) def deleteTool(self, name): try: del self.tortoisehgtools[name] except KeyError: pass def applyChanges(self, ini): # widget.value() returns the _NEW_ values # widget.curvalue returns the _ORIGINAL_ values (yes, this is a bit # misleading! "cur" means "current" as in currently valid) def updateIniValue(section, key, newvalue): section = hglib.fromunicode(section) key = hglib.fromunicode(key) try: del ini[section][key] except KeyError: pass if newvalue is not None: ini.set(section, key, newvalue) emitChanged = False if not self.isDirty(): return emitChanged emitChanged = True # 1. Save the new tool configurations # # In order to keep the tool order we must delete all existing # custom tool configurations, and then set all the configuration # settings anew: section = 'tortoisehg-tools' fieldnames = ('command', 'workingdir', 'label', 'tooltip', 'icon', 'location', 'enable', 'showoutput',) for name in self.curvalue: for field in fieldnames: try: keyname = '%s.%s' % (name, field) del ini[section][keyname] except KeyError: pass tools = self.value() for uname in tools: name = hglib.fromunicode(uname) if name[0] in '|-': continue for field in sorted(tools[name]): keyname = '%s.%s' % (name, field) value = tools[name][field] if not value is '': ini.set(section, keyname, value) # 2. Save the new guidefs for n, toollistwidget in enumerate(self.widgets): toollocation = self.locationcombo.itemData(n).toString() if not toollistwidget.isDirty(): continue emitChanged = True toollist = toollistwidget.value() updateIniValue('tortoisehg', toollocation, ' '.join(toollist)) return emitChanged ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = dict(curvalue) def value(self): return self.tortoisehgtools def isDirty(self): for toollistwidget in self.widgets: if toollistwidget.isDirty(): return True if self.globaltoollist.isDirty(): return True return self.tortoisehgtools != self.curvalue def refresh(self): self.tortoisehgtools, guidef = hglib.tortoisehgtools(self.ini) self.setValue(self.tortoisehgtools) self.globaltoollist.refresh() for w in self.widgets: w.refresh() class HooksFrame(QFrame): def __init__(self, ini, parent=None, **opts): super(HooksFrame, self).__init__(parent, **opts) self.ini = ini # The frame is created empty, and will be populated on 'refresh', # which usually happens when the frames is activated self.setValue({}) topbox = QHBoxLayout() self.setLayout(topbox) self.hooktable = QTableWidget(0, 3, parent) self.hooktable.setHorizontalHeaderLabels((_('Type'), _('Name'), _('Command'))) self.hooktable.sortByColumn(0, Qt.AscendingOrder) self.hooktable.setSelectionBehavior(self.hooktable.SelectRows) self.hooktable.setSelectionMode(self.hooktable.SingleSelection) self.hooktable.cellDoubleClicked.connect(self.editHook) topbox.addWidget(self.hooktable) buttonbox = QVBoxLayout() self.btnnew = QPushButton(_('New hook')) buttonbox.addWidget(self.btnnew) self.btnnew.clicked.connect(self.newHook) self.btnedit = QPushButton(_('Edit hook')) buttonbox.addWidget(self.btnedit) self.btnedit.clicked.connect(self.editCurrentHook) self.btndelete = QPushButton(_('Delete hook')) self.btndelete.clicked.connect(self.deleteCurrentHook) buttonbox.addWidget(self.btndelete) buttonbox.addStretch() topbox.addLayout(buttonbox) def newHook(self): td = HookConfigDialog(self) res = td.exec_() if res: hooktype, command, hookname = td.value() # Does the new hook already exist? hooks = self.value() if hooktype in hooks: existingcommand = hooks[hooktype].get(hookname, None) if existingcommand is not None: if existingcommand == command: # The command already exists "as is"! return if not qtlib.QuestionMsgBox( _('Replace existing hook?'), _('There is an existing %s.%s hook.\n\n' 'Do you want to replace it?') % (hooktype, hookname), parent=self): return # Delete existing matching hooks in reverse order # (otherwise the row numbers will be wrong after the first # deletion) for r in reversed(self.findHooks( hooktype=hooktype, hookname=hookname)): self.deleteHook(r) self.hooktable.setSortingEnabled(False) row = self.hooktable.rowCount() self.hooktable.insertRow(row) for c, text in enumerate((hooktype, hookname, command)): self.hooktable.setItem(row, c, QTableWidgetItem(text)) # Make the hook column not editable (a dialog is used to edit it) itemhook = self.hooktable.item(row, 0) itemhook.setFlags(itemhook.flags() & ~Qt.ItemIsEditable) self.hooktable.setSortingEnabled(True) self.hooktable.resizeColumnsToContents() self.updatebuttons() def editHook(self, r, c=0): if r < 0: r = 0 numrows = self.hooktable.rowCount() if not numrows or r >= numrows: return False if c > 0: # Only show the edit dialog when clicking # on the "Hook Type" (i.e. the 1st) column return False hooktype = self.hooktable.item(r, 0).text() hookname = self.hooktable.item(r, 1).text() command = self.hooktable.item(r, 2).text() td = HookConfigDialog(self, hooktype=hooktype, command=command, hookname=hookname) res = td.exec_() if res: hooktype, command, hookname = td.value() # Update the table # Note that we must disable the ordering while the table # is updated to avoid updating the wrong cell! self.hooktable.setSortingEnabled(False) self.hooktable.item(r, 0).setText(hooktype) self.hooktable.item(r, 1).setText(hookname) self.hooktable.item(r, 2).setText(command) self.hooktable.setSortingEnabled(True) self.hooktable.clearSelection() self.hooktable.setState(self.hooktable.NoState) self.hooktable.resizeColumnsToContents() return bool(res) def editCurrentHook(self): self.editHook(self.hooktable.currentRow()) def deleteHook(self, row=None): if row is None: row = self.hooktable.currentRow() if row < 0: row = self.hooktable.rowCount() - 1 self.hooktable.removeRow(row) self.hooktable.resizeColumnsToContents() self.updatebuttons() def deleteCurrentHook(self): self.deleteHook() def findHooks(self, hooktype=None, hookname=None, command=None): matchingrows = [] for r in range(self.hooktable.rowCount()): currhooktype = hglib.fromunicode(self.hooktable.item(r, 0).text()) currhookname = hglib.fromunicode(self.hooktable.item(r, 1).text()) currcommand = hglib.fromunicode(self.hooktable.item(r, 2).text()) matchinghooktype = hooktype is None or hooktype == currhooktype matchinghookname = hookname is None or hookname == currhookname matchingcommand = command is None or command == currcommand if matchinghooktype and matchinghookname and matchingcommand: matchingrows.append(r) return matchingrows def updatebuttons(self): tablehasitems = self.hooktable.rowCount() > 0 self.btnedit.setEnabled(tablehasitems) self.btndelete.setEnabled(tablehasitems) def applyChanges(self, ini): # widget.value() returns the _NEW_ values # widget.curvalue returns the _ORIGINAL_ values (yes, this is a bit # misleading! "cur" means "current" as in currently valid) emitChanged = False if not self.isDirty(): return emitChanged emitChanged = True # 1. Delete the previous hook configurations section = 'hooks' hooks = self.curvalue for hooktype in hooks: for keyname in hooks[hooktype]: try: keyname = '%s.%s' % (hooktype, keyname) del ini[section][keyname] except KeyError: pass # 2. Save the new configurations hooks = self.value() for hooktype in hooks: for field in sorted(hooks[hooktype]): if field: keyname = '%s.%s' % (hooktype, field) else: keyname = hooktype value = hooks[hooktype][field] if value: ini.set(section, keyname, value) return emitChanged ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = dict(curvalue) def value(self): hooks = {} for r in range(self.hooktable.rowCount()): hooktype = hglib.fromunicode(self.hooktable.item(r, 0).text()) hookname = hglib.fromunicode(self.hooktable.item(r, 1).text()) command = hglib.fromunicode(self.hooktable.item(r, 2).text()) if hooktype not in hooks: hooks[hooktype] = {} hooks[hooktype][hookname] = command return hooks def isDirty(self): return self.value() != self.curvalue def gethooks(self): hooks = {} for key, value in self.ini.items('hooks'): keyparts = key.split('.', 1) hooktype = keyparts[0] if len(keyparts) == 1: name = '' else: name = keyparts[1] if hooktype not in hooks: hooks[hooktype] = {} hooks[hooktype][name] = value return hooks def refresh(self): hooks = self.gethooks() self.setValue(hooks) self.hooktable.setSortingEnabled(False) self.hooktable.setRowCount(0) for hooktype in sorted(hooks): for name in sorted(hooks[hooktype]): itemhook = QTableWidgetItem(hglib.tounicode(hooktype)) # Make the hook column not editable # (a dialog is used to edit it) itemhook.setFlags(itemhook.flags() & ~Qt.ItemIsEditable) itemname = QTableWidgetItem(hglib.tounicode(name)) itemtool = QTableWidgetItem( hglib.tounicode(hooks[hooktype][name])) self.hooktable.insertRow(self.hooktable.rowCount()) self.hooktable.setItem(self.hooktable.rowCount() - 1, 0, itemhook) self.hooktable.setItem(self.hooktable.rowCount() - 1, 1, itemname) self.hooktable.setItem(self.hooktable.rowCount() - 1, 2, itemtool) self.hooktable.setSortingEnabled(True) self.hooktable.resizeColumnsToContents() self.updatebuttons() class ToolListBox(QListWidget): SEPARATOR = '------' def __init__(self, ini, parent=None, location=None, minimumwidth=None, **opts): QListWidget.__init__(self, parent, **opts) self.opts = opts self.curvalue = None self.ini = ini self.location = location if minimumwidth: self.setMinimumWidth(minimumwidth) self.refresh() # Enable drag and drop to reorder the tools self.setDragEnabled(True) self.setDragDropMode(self.InternalMove) if PYQT_VERSION >= 0x40700: self.setDefaultDropAction(Qt.MoveAction) def _guidef2toollist(self, guidef): toollist = [] for name in guidef: if name == '|': name = self.SEPARATOR # avoid putting multiple separators together if [name] == toollist[-1:]: continue toollist.append(name) return toollist def _toollist2guidef(self, toollist): guidef = [] for uname in toollist: if uname == self.SEPARATOR: name = '|' # avoid putting multiple separators together if [name] == toollist[-1:]: continue else: name = hglib.fromunicode(uname) guidef.append(name) return guidef def addOrInsertItem(self, text, icon=None): if text == self.SEPARATOR: item = text else: if not icon: icon = CustomToolConfigDialog._defaulticonname if isinstance(icon, str): icon = qtlib.geticon(icon) item = QListWidgetItem(icon, text) row = self.currentIndex().row() if row < 0: self.addItem(item) self.setCurrentRow(self.count()-1) else: self.insertItem(row+1, item) self.setCurrentRow(row+1) def deleteTool(self, row=None, remove=False): if row is None: row = self.currentIndex().row() if row >= 0: self.takeItem(row) def addSeparator(self): self.addOrInsertItem(self.SEPARATOR, icon=None) def values(self): out = [] for row in range(self.count()): out.append(self.item(row).text()) return out ## common APIs for all edit widgets def setValue(self, curvalue): self.curvalue = curvalue def value(self): return self._toollist2guidef(self.values()) def isDirty(self): return self.value() != self.curvalue def refresh(self): toolsdefs, guidef = hglib.tortoisehgtools(self.ini, selectedlocation=self.location) self.toollist = self._guidef2toollist(guidef) self.setValue(guidef) self.clear() for toolname in self.toollist: icon = toolsdefs.get(toolname, {}).get('icon', None) self.addOrInsertItem(toolname, icon=icon) def removeInvalid(self, validtools): validguidef = [] for toolname in self.value(): if toolname[0] not in '|-': if toolname not in validtools: continue validguidef.append(toolname) self.clear() self.toollist = self._guidef2toollist(validguidef) for toolname in self.toollist: icon = validtools.get(toolname, {}).get('icon', None) self.addOrInsertItem(toolname, icon=icon) class CustomConfigDialog(QDialog): '''Custom Config Dialog base class''' def __init__(self, parent=None, dialogname='', **kwargs): QDialog.__init__(self, parent, **kwargs) self.dialogname = dialogname self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.hbox = QHBoxLayout() self.formvbox = QFormLayout() self.hbox.addLayout(self.formvbox) vbox = QVBoxLayout() self.okbutton = QPushButton(_('OK')) self.okbutton.clicked.connect(self.okClicked) vbox.addWidget(self.okbutton) self.cancelbutton = QPushButton(_('Cancel')) self.cancelbutton.clicked.connect(self.reject) vbox.addWidget(self.cancelbutton) vbox.addStretch() self.hbox.addLayout(vbox) self.setLayout(self.hbox) self.setMaximumHeight(self.sizeHint().height()) self._readsettings() def value(self): return None def _genCombo(self, items, selecteditem=None): index = 0 if selecteditem: try: index = list(items).index(selecteditem) except ValueError: pass combo = QComboBox() combo.addItems(items) if index: combo.setCurrentIndex(index) return combo def _addConfigItem(self, parent, label, configwidget, tooltip=None): if tooltip: configwidget.setToolTip(tooltip) parent.addRow(label, configwidget) return configwidget def okClicked(self): errormsg = self.validateForm() if errormsg: qtlib.WarningMsgBox(_('Missing information'), errormsg) return return self.accept() def validateForm(self): return '' # No error def _readsettings(self): s = QSettings() if self.dialogname: self.restoreGeometry(s.value(self.dialogname + '/geom').toByteArray()) return s def _writesettings(self): s = QSettings() if self.dialogname: s.setValue(self.dialogname + '/geom', self.saveGeometry()) def done(self, r): self._writesettings() super(CustomConfigDialog, self).done(r) class CustomToolConfigDialog(CustomConfigDialog): '''Dialog for editing custom tool configurations''' _enablemappings = [(_('All items'), 'istrue'), (_('Working directory'), 'iswd'), (_('All revisions'), 'isrev'), (_('All contexts'), 'isctx'), (_('Fixed revisions'), 'fixed'), (_('Applied patches'), 'applied'), (_('Applied patches or qparent'), 'qgoto'), ] _defaulticonname = 'tools-spanner-hammer' _defaulticonstring = _('') def __init__(self, parent=None, toolname=None, toolconfig={}): super(CustomToolConfigDialog, self).__init__(parent, dialogname='customtools', windowTitle=_('Configure Custom Tool'), windowIcon=qtlib.geticon(self._defaulticonname)) vbox = self.formvbox command = toolconfig.get('command', '') workingdir = toolconfig.get('workingdir', '') label = toolconfig.get('label', '') tooltip = toolconfig.get('tooltip', '') ico = toolconfig.get('icon', '') enable = toolconfig.get('enable', 'all') showoutput = str(toolconfig.get('showoutput', False)) self.name = self._addConfigItem(vbox, _('Tool name'), QLineEdit(toolname), _('The tool name. It cannot contain spaces.')) # Execute a mercurial command. These _MUST_ start with "hg" self.command = self._addConfigItem(vbox, _('Command'), QLineEdit(command), _('The command that will be executed.\n' 'To execute a Mercurial command use "hg" (rather than "hg.exe") ' 'as the executable command.\n' 'You can use several {VARIABLES} to compose your command:\n' '- {ROOT}: The path to the current repository root.\n' '- {REV} / {REVID}: the selected revision number / ' 'hexadecimal revision id hash respectively.\n' '- {FILES}: The list of files touched by the selected revision.\n' '- {ALLFILES}: All the files tracked by Mercurial on the selected' ' revision.')) self.workingdir = self._addConfigItem(vbox, _('Working Directory'), QLineEdit(workingdir), _('The directory where the command will be executed.\n' 'If this is not set, the root of the current repository ' 'will be used instead.\n' 'You can use the same {VARIABLES} as on the "Command" setting.\n')) self.label = self._addConfigItem(vbox, _('Tool label'), QLineEdit(label), _('The tool label, which is what will be shown ' 'on the repowidget context menu.\n' 'If no label is set, the tool name will be used as the tool label.\n' 'If no tooltip is set, the label will be used as the tooltip as well.')) self.tooltip = self._addConfigItem(vbox, _('Tooltip'), QLineEdit(tooltip), _('The tooltip that will be shown on the tool button.\n' 'This is only shown when the tool button is shown on\n' 'the workbench toolbar.')) iconnames = qtlib.getallicons() combo = QComboBox() if not ico: ico = self._defaulticonstring elif ico not in iconnames: combo.addItem(qtlib.geticon(ico), ico) combo.addItem(qtlib.geticon(self._defaulticonname), self._defaulticonstring) for name in iconnames: combo.addItem(qtlib.geticon(name), name) combo.setEditable(True) idx = combo.findText(ico) # note that idx will always be >= 0 because if ico not in iconnames # it will have been added as the first element on the combobox! combo.setCurrentIndex(idx) self.icon = self._addConfigItem(vbox, _('Icon'), combo, _('The tool icon.\n' 'You can use any built-in TortoiseHg icon\n' 'by setting this value to a valid TortoiseHg icon name\n' '(e.g. clone, add, remove, sync, thg-logo, hg-update, etc).\n' 'You can also set this value to the absolute path to\n' 'any icon on your file system.')) combo = self._genCombo([l for l, _v in self._enablemappings], self._enable2label(enable)) self.enable = self._addConfigItem(vbox, _('On repowidget, show for'), combo, _('For which kinds of revisions the tool will be enabled\n' 'It is only taken into account when the tool is shown on the\n' 'selected revision context menu.')) combo = self._genCombo(('True', 'False'), showoutput) self.showoutput = self._addConfigItem(vbox, _('Show Output Log'), combo, _('When enabled, automatically show the Output Log when the ' 'command is run.\nDefault: False.')) def value(self): toolname = str(self.name.text()).strip() toolconfig = { 'label': str(self.label.text()), 'command': str(self.command.text()), 'workingdir': str(self.workingdir.text()), 'tooltip': str(self.tooltip.text()), 'icon': str(self.icon.currentText()), 'enable': self._enablemappings[self.enable.currentIndex()][1], 'showoutput': str(self.showoutput.currentText()), } if toolconfig['icon'] == self._defaulticonstring: toolconfig['icon'] = '' return toolname, toolconfig def _enable2label(self, value): return dict((v, l) for l, v in self._enablemappings).get(value) def validateForm(self): name, config = self.value() if not name: return _('You must set a tool name.') if name.find(' ') >= 0: return _('The tool name cannot have any spaces in it.') if not config['command']: return _('You must set a command to run.') return '' # No error class HookConfigDialog(CustomConfigDialog): '''Dialog for editing the a hook configuration''' _hooktypes = ( 'changegroup', 'commit', 'incoming', 'outgoing', 'prechangegroup', 'precommit', 'prelistkeys', 'preoutgoing', 'prepushkey', 'pretag', 'pretxnchangegroup', 'pretxncommit', 'preupdate', 'listkeys', 'pushkey', 'tag', 'update', ) _rehookname = re.compile('^[^=\s]*$') def __init__(self, parent=None, hooktype=None, command='', hookname=''): super(HookConfigDialog, self).__init__(parent, dialogname='hookconfigdialog', windowTitle=_('Configure Hook'), windowIcon=qtlib.geticon('tools-hooks')) vbox = self.formvbox combo = self._genCombo(self._hooktypes, hooktype) self.hooktype = self._addConfigItem(vbox, _('Hook type'), combo, _('Select when your command will be run')) self.name = self._addConfigItem(vbox, _('Tool name'), QLineEdit(hookname), _('The hook name. It cannot contain spaces.')) self.command = self._addConfigItem(vbox, _('Command'), QLineEdit(command), _('The command that will be executed.\n' 'To execute a python function prepend the command with ' '"python:".\n')) def value(self): hooktype = str(self.hooktype.currentText()) hookname = str(self.name.text()).strip() command = str(self.command.text()).strip() return hooktype, command, hookname def validateForm(self): hooktype, command, hookname = self.value() if hooktype not in self._hooktypes: return _('You must set a valid hook type.') if self._rehookname.match(hookname) is None: return _('The hook name cannot contain any spaces, ' 'tabs or \'=\' characters.') if not command: return _('You must set a command to run.') return '' # No error def addCustomToolsSubmenu(menu, ui, location, make, slot, label=_('Custom Tools')): ''' Add a custom tools submenu to an existing menus This can be used, for example, to add the custom tools submenu to the different file context menus ''' tools, toollist = hglib.tortoisehgtools(ui, selectedlocation=location) if not tools: return submenu = menu.addMenu(label) submenu.triggered.connect(slot) emptysubmenu = True for name in toollist: if name == '|': submenu.addSeparator() continue info = tools.get(name, None) if info is None: continue command = info.get('command', None) if not command: continue label = info.get('label', name) icon = info.get('icon', CustomToolConfigDialog._defaulticonname) status = info.get('status', 'MAR!C?S') a = make(label, None, frozenset(status), icon=icon, inmenu=submenu) if a is not None: a.setData(name) emptysubmenu = False if emptysubmenu: menu.removeAction(submenu.menuAction()) tortoisehg-2.10/tortoisehg/hgqt/backout.py0000644000076400007640000005501412231647662020043 0ustar stevesteve# backout.py - Backout dialog for TortoiseHg # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, csinfo, i18n, cmdui, status, resolve from tortoisehg.hgqt import qscilib, thgrepo, messageentry, wctxcleaner from PyQt4.QtCore import * from PyQt4.QtGui import * class BackoutDialog(QWizard): def __init__(self, repoagent, rev, parent=None): super(BackoutDialog, self).__init__(parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self.backoutrev = rev self.parentbackout = False self.backoutmergeparentrev = None repo = repoagent.rawRepo() self.setWindowTitle(_('Backout - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-revert')) self.setOption(QWizard.NoBackButtonOnStartPage, True) self.setOption(QWizard.NoBackButtonOnLastPage, True) self.setOption(QWizard.IndependentPages, True) self.addPage(SummaryPage(repoagent, self)) self.addPage(BackoutPage(repoagent, self)) self.addPage(CommitPage(repoagent, self)) self.addPage(ResultPage(repoagent, self)) self.currentIdChanged.connect(self.pageChanged) self.resize(QSize(700, 489).expandedTo(self.minimumSizeHint())) repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.configChanged.connect(self.configChanged) @pyqtSlot() def repositoryChanged(self): self.currentPage().repositoryChanged() @pyqtSlot() def configChanged(self): self.currentPage().configChanged() def pageChanged(self, id): if id != -1: self.currentPage().currentPage() def reject(self): if self.currentPage().canExit(): super(BackoutDialog, self).reject() class BasePage(QWizardPage): def __init__(self, repoagent, parent): super(BasePage, self).__init__(parent) self._repoagent = repoagent @property def repo(self): return self._repoagent.rawRepo() def validatePage(self): 'user pressed NEXT button, can we proceed?' return True def isComplete(self): 'should NEXT button be sensitive?' return True def repositoryChanged(self): 'repository has detected a change to changelog or parents' pass def configChanged(self): 'repository has detected a change to config files' pass def currentPage(self): pass def canExit(self): return True class SummaryPage(BasePage): def __init__(self, repoagent, parent): super(SummaryPage, self).__init__(repoagent, parent) self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkStarted.connect(self._onCheckStarted) self._wctxcleaner.checkFinished.connect(self._onCheckFinished) def initializePage(self): if self.layout(): return self.setTitle(_('Prepare to backout')) self.setSubTitle(_('Verify backout revision and ensure your working ' 'directory is clean.')) self.setLayout(QVBoxLayout()) self.groups = qtlib.WidgetGroups() repo = self.repo try: bctx = repo[self.wizard().backoutrev] pctx = repo['.'] except error.RepoLookupError: qtlib.InfoMsgBox(_('Unable to backout'), _('Backout revision not found')) QTimer.singleShot(0, self.wizard().close) return if pctx == bctx: lbl = _('Backing out a parent revision is a single step operation') self.layout().addWidget(QLabel(u'%s' % lbl)) self.wizard().parentbackout = True op1, op2 = repo.dirstate.parents() if op1 is None: qtlib.InfoMsgBox(_('Unable to backout'), _('Backout requires a parent revision')) QTimer.singleShot(0, self.wizard().close) return a = repo.changelog.ancestor(op1, bctx.node()) if a != bctx.node(): qtlib.InfoMsgBox(_('Unable to backout'), _('Cannot backout change on a different branch')) QTimer.singleShot(0, self.wizard().close) ## backout revision style = csinfo.panelstyle(contents=csinfo.PANEL_DEFAULT) create = csinfo.factory(repo, None, style, withupdate=True) sep = qtlib.LabeledSeparator(_('Backout revision')) self.layout().addWidget(sep) backoutCsInfo = create(bctx.rev()) self.layout().addWidget(backoutCsInfo) ## current revision contents = ('ishead',) + csinfo.PANEL_DEFAULT style = csinfo.panelstyle(contents=contents) def markup_func(widget, item, value): if item == 'ishead' and value is False: text = _('Not a head, backout will create a new head!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) custom = csinfo.custom(markup=markup_func) create = csinfo.factory(repo, custom, style, withupdate=True) sep = qtlib.LabeledSeparator(_('Current local revision')) self.layout().addWidget(sep) localCsInfo = create(pctx.rev()) self.layout().addWidget(localCsInfo) self.localCsInfo = localCsInfo ## Merge revision backout handling if len(bctx.parents()) > 1: # Show two radio buttons letting the user which merge revision # parent to backout to p1rev = bctx.p1().rev() p2rev = bctx.p2().rev() def setBackoutMergeParentRev(rev): self.wizard().backoutmergeparentrev = rev setBackoutMergeParentRev(p1rev) sep = qtlib.LabeledSeparator(_('Merge parent to backout to')) self.layout().addWidget(sep) self.layout().addWidget(QLabel( _('To backout a merge revision you must select which ' 'parent to backout to ' '(i.e. whose changes will be kept)'))) self.actionFirstParent = QRadioButton( _('First Parent: revision %s (%s)') \ % (p1rev, str(bctx.p1())), self) self.actionFirstParent.setCheckable(True) self.actionFirstParent.setChecked(True) self.actionFirstParent.setShortcut('CTRL+1') self.actionFirstParent.setToolTip( _('Backout to the first parent of the merge revision')) self.actionFirstParent.clicked.connect( lambda: setBackoutMergeParentRev(p1rev)) self.actionSecondParent = QRadioButton( _('Second Parent: revision %s (%s)') % (p2rev, str(bctx.p2())), self) self.actionSecondParent.setCheckable(True) self.actionSecondParent.setShortcut('CTRL+2') self.actionSecondParent.setToolTip( _('Backout to the second parent of the merge revision')) self.actionSecondParent.clicked.connect( lambda: setBackoutMergeParentRev(p2rev)) self.layout().addWidget(self.actionFirstParent) self.layout().addWidget(self.actionSecondParent) ## working directory status sep = qtlib.LabeledSeparator(_('Working directory status')) self.layout().addWidget(sep) wdbox = QHBoxLayout() self.layout().addLayout(wdbox) self.wd_status = qtlib.StatusLabel() self.wd_status.set_status(_('Checking...')) wdbox.addWidget(self.wd_status) wd_prog = QProgressBar() wd_prog.setMaximum(0) wd_prog.setTextVisible(False) self.groups.add(wd_prog, 'prog') wdbox.addWidget(wd_prog, 1) text = _('Before backout, you must commit, ' 'shelve to patch, ' 'or discard changes.') wd_text = QLabel(text) wd_text.setWordWrap(True) wd_text.linkActivated.connect(self._wctxcleaner.runCleaner) self.wd_text = wd_text self.groups.add(wd_text, 'dirty') self.layout().addWidget(wd_text) ## auto-resolve autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) autoresolve_chk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', False)) self.registerField('autoresolve', autoresolve_chk) self.layout().addWidget(autoresolve_chk) self.autoresolve_chk = autoresolve_chk self.groups.set_visible(False, 'dirty') def isComplete(self): 'should Next button be sensitive?' return self._wctxcleaner.isClean() def repositoryChanged(self): 'repository has detected a change to changelog or parents' pctx = self.repo['.'] self.localCsInfo.update(pctx) self.wizard().localrev = str(pctx.rev()) def canExit(self): 'can backout tool be closed?' if self._wctxcleaner.isChecking(): self._wctxcleaner.cancelCheck() return True def currentPage(self): self.refresh() def refresh(self): self._wctxcleaner.check() @pyqtSlot() def _onCheckStarted(self): self.groups.set_visible(True, 'prog') @pyqtSlot(bool) def _onCheckFinished(self, clean): self.groups.set_visible(False, 'prog') if self._wctxcleaner.isCheckCanceled(): return if not clean: self.groups.set_visible(True, 'dirty') self.wd_status.set_status(_('Uncommitted local changes ' 'are detected'), 'thg-warning') else: self.groups.set_visible(False, 'dirty') self.wd_status.set_status(_('Clean'), True) self.completeChanged.emit() class BackoutPage(BasePage): def __init__(self, repoagent, parent): super(BackoutPage, self).__init__(repoagent, parent) self.backoutcomplete = False self.setTitle(_('Backing out, then merging...')) self.setSubTitle(_('All conflicting files will be marked unresolved.')) self.setLayout(QVBoxLayout()) self.cmd = cmdui.Widget(True, False, self) self.cmd.commandFinished.connect(self.onCommandFinished) self.cmd.setShowOutput(True) self.layout().addWidget(self.cmd) self.reslabel = QLabel() self.reslabel.linkActivated.connect(self.onLinkActivated) self.reslabel.setWordWrap(True) self.layout().addWidget(self.reslabel) self.autonext = QCheckBox(_('Automatically advance to next page ' 'when backout and merge are complete.')) checked = QSettings().value('backout/autoadvance', False).toBool() self.autonext.setChecked(checked) self.autonext.toggled.connect(self.tryAutoAdvance) self.layout().addWidget(self.autonext) def currentPage(self): if self.wizard().parentbackout: self.wizard().next() return cmdline = ['--repository', self.repo.root, 'backout'] tool = self.field('autoresolve').toBool() and 'merge' or 'fail' cmdline += ['--tool=internal:' + tool] cmdline += ['--rev', str(self.wizard().backoutrev)] if self.wizard().backoutmergeparentrev: cmdline += ['--parent', str(self.wizard().backoutmergeparentrev)] self.repo.incrementBusyCount() self.cmd.core.clearOutput() self.cmd.run(cmdline) def isComplete(self): 'should Next button be sensitive?' if not self.backoutcomplete: return False count = 0 for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': count += 1 if count: # if autoresolve is enabled, we know these were real conflicts self.reslabel.setText(_('%d files have merge conflicts ' 'that must be ' 'resolved') % count) return False else: self.reslabel.setText(_('No merge conflicts, ready to commit')) return True def tryAutoAdvance(self, checked): if checked and self.isComplete(): self.wizard().next() def cleanupPage(self): QSettings().setValue('backout/autoadvance', self.autonext.isChecked()) def onCommandFinished(self, ret): self.repo.decrementBusyCount() if ret in (0, 1): self.backoutcomplete = True if self.autonext.isChecked(): self.tryAutoAdvance(True) self.completeChanged.emit() @pyqtSlot(QString) def onLinkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() if self.autonext.isChecked(): self.tryAutoAdvance(True) self.completeChanged.emit() class CommitPage(BasePage): def __init__(self, repoagent, parent): super(CommitPage, self).__init__(repoagent, parent) self.commitComplete = False self.setTitle(_('Commit backout and merge results')) self.setSubTitle(' ') self.setLayout(QVBoxLayout()) self.setCommitPage(True) repo = repoagent.rawRepo() # csinfo def label_func(widget, item, ctx): if item == 'rev': return _('Revision:') elif item == 'parents': return _('Parents') raise csinfo.UnknownItem() def data_func(widget, item, ctx): if item == 'rev': return _('Working Directory'), str(ctx) elif item == 'parents': parents = [] cbranch = ctx.branch() for pctx in ctx.parents(): branch = None if hasattr(pctx, 'branch') and pctx.branch() != cbranch: branch = pctx.branch() parents.append((str(pctx.rev()), str(pctx), branch, pctx)) return parents raise csinfo.UnknownItem() def markup_func(widget, item, value): if item == 'rev': text, rev = value if self.wizard() and self.wizard().parentbackout: return '%s (%s)' % (text, rev) else: return '%s (%s)' % (text, rev) elif item == 'parents': def branch_markup(branch): opts = dict(fg='black', bg='#aaffaa') return qtlib.markup(' %s ' % branch, **opts) csets = [] for rnum, rid, branch, pctx in value: line = '%s (%s)' % (rnum, rid) if branch: line = '%s %s' % (line, branch_markup(branch)) msg = widget.info.get_data('summary', widget, pctx, widget.custom) if msg: line = '%s %s' % (line, msg) csets.append(line) return csets raise csinfo.UnknownItem() custom = csinfo.custom(label=label_func, data=data_func, markup=markup_func) contents = ('rev', 'user', 'dateage', 'branch', 'parents') style = csinfo.panelstyle(contents=contents, margin=6) # merged files rev_sep = qtlib.LabeledSeparator(_('Working Directory (merged)')) self.layout().addWidget(rev_sep) bkCsInfo = csinfo.create(repo, None, style, custom=custom, withupdate=True) bkCsInfo.linkActivated.connect(self.onLinkActivated) self.layout().addWidget(bkCsInfo) # commit message area msg_sep = qtlib.LabeledSeparator(_('Commit message')) self.layout().addWidget(msg_sep) msgEntry = messageentry.MessageEntry(self) msgEntry.installEventFilter(qscilib.KeyPressInterceptor(self)) msgEntry.refresh(repo) msgEntry.loadSettings(QSettings(), 'backout/message') msgEntry.textChanged.connect(self.completeChanged) self.layout().addWidget(msgEntry) self.msgEntry = msgEntry self.cmd = cmdui.Widget(True, False, self) self.cmd.commandFinished.connect(self.onCommandFinished) self.cmd.setShowOutput(False) self.layout().addWidget(self.cmd) def tryperform(): if self.isComplete(): self.wizard().next() actionEnter = QAction('alt-enter', self) actionEnter.setShortcuts([Qt.CTRL+Qt.Key_Return, Qt.CTRL+Qt.Key_Enter]) actionEnter.triggered.connect(tryperform) self.addAction(actionEnter) self.skiplast = QCheckBox(_('Skip final confirmation page, ' 'close after commit.')) checked = QSettings().value('backout/skiplast', False).toBool() self.skiplast.setChecked(checked) self.layout().addWidget(self.skiplast) def eng_toggled(checked): if self.isComplete(): oldmsg = self.msgEntry.text() if self.wizard().backoutmergeparentrev: msgset = i18n.keepgettext()._( 'Backed out merge changeset: ') else: msgset = i18n.keepgettext()._('Backed out changeset: ') msg = checked and msgset['id'] or msgset['str'] if oldmsg and oldmsg != msg: if not qtlib.QuestionMsgBox(_('Confirm Discard Message'), _('Discard current backout message?'), parent=self): self.engChk.blockSignals(True) self.engChk.setChecked(not checked) self.engChk.blockSignals(False) return self.msgEntry.setText(msg + str(self.repo[self.wizard().backoutrev])) self.msgEntry.moveCursorToEnd() self.engChk = QCheckBox(_('Use English backout message')) self.engChk.toggled.connect(eng_toggled) engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False) self.engChk.setChecked(engmsg) self.layout().addWidget(self.engChk) def refresh(self): pass def cleanupPage(self): s = QSettings() s.setValue('backout/skiplast', self.skiplast.isChecked()) self.msgEntry.saveSettings(s, 'backout/message') def currentPage(self): engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False) mergeparentrev = self.wizard().backoutmergeparentrev if mergeparentrev: msgset = i18n.keepgettext()._( 'Backed out merge changeset: ') else: msgset = i18n.keepgettext()._('Backed out changeset: ') msg = engmsg and msgset['id'] or msgset['str'] msg += str(self.repo[self.wizard().backoutrev]) if mergeparentrev: msg += '\n\n' bctx = self.repo[self.wizard().backoutrev] isp1 = (bctx.p1().rev() == mergeparentrev) if isp1: msg += _('Backed out merge revision ' 'to its first parent (%s)') % str(bctx.p1()) else: msg += _('Backed out merge revision ' 'to its second parent (%s)') % str(bctx.p2()) self.msgEntry.setText(msg) self.msgEntry.moveCursorToEnd() @pyqtSlot(QString) def onLinkActivated(self, cmd): if cmd == 'view': dlg = status.StatusDialog(self._repoagent, [], {}, self) dlg.exec_() self.refresh() def isComplete(self): return len(self.msgEntry.text()) > 0 def validatePage(self): if self.commitComplete: # commit succeeded, repositoryChanged() called wizard().next() if self.skiplast.isChecked(): self.wizard().close() return True if self.cmd.core.running(): return False user = qtlib.getCurrentUsername(self, self.repo) if not user: return False if self.wizard().parentbackout: self.setTitle(_('Backing out and committing...')) self.setSubTitle(_('Please wait while making backout.')) message = hglib.fromunicode(self.msgEntry.text()) cmdline = ['backout', '--verbose', '--message', message, '--rev', str(self.wizard().backoutrev), '--user', user, '--repository', self.repo.root] if self.wizard().backoutmergeparentrev: cmdline += ['--parent', str(self.wizard().backoutmergeparentrev)] else: self.setTitle(_('Committing...')) self.setSubTitle(_('Please wait while committing merged files.')) message = hglib.fromunicode(self.msgEntry.text()) cmdline = ['commit', '--verbose', '--message', message, '--repository', self.repo.root, '--user', user] commandlines = [cmdline] pushafter = self.repo.ui.config('tortoisehg', 'cipushafter') if pushafter: cmd = ['push', '--repository', self.repo.root, pushafter] commandlines.append(cmd) self.repo.incrementBusyCount() self.cmd.setShowOutput(True) self.cmd.run(*commandlines) return False def onCommandFinished(self, ret): self.repo.decrementBusyCount() if ret == 0: self.commitComplete = True self.wizard().next() class ResultPage(BasePage): def __init__(self, repoagent, parent): super(ResultPage, self).__init__(repoagent, parent) self.setTitle(_('Finished')) self.setSubTitle(' ') self.setFinalPage(True) self.setLayout(QVBoxLayout()) sep = qtlib.LabeledSeparator(_('Backout changeset')) self.layout().addWidget(sep) bkCsInfo = csinfo.create(self.repo, 'tip', withupdate=True) self.layout().addWidget(bkCsInfo) self.bkCsInfo = bkCsInfo self.layout().addStretch(1) def currentPage(self): self.bkCsInfo.update(self.repo['tip']) self.wizard().setOption(QWizard.NoCancelButton, True) tortoisehg-2.10/tortoisehg/hgqt/tag.py0000644000076400007640000002764512231647662017177 0ustar stevesteve# tag.py - Tag dialog for TortoiseHg # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, qtlib, i18n from PyQt4.QtCore import * from PyQt4.QtGui import * keep = i18n.keepgettext() class TagDialog(QDialog): def __init__(self, repoagent, tag='', rev='tip', parent=None, opts={}): super(TagDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self.setWindowTitle(_('Tag - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-tag')) # base layout box base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(*(0,)*4) base.setSizeConstraint(QLayout.SetFixedSize) self.setLayout(base) # main layout box box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(8,)*4) self.layout().addLayout(box) form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) box.addLayout(form) ctx = repo[rev] form.addRow(_('Revision:'), QLabel('%d (%s)' % (ctx.rev(), ctx))) self.rev = ctx.rev() ### tag combo self.tagCombo = QComboBox() self.tagCombo.setEditable(True) self.tagCombo.setEditText(hglib.tounicode(tag)) self.tagCombo.currentIndexChanged.connect(self.updateStates) self.tagCombo.editTextChanged.connect(self.updateStates) qtlib.allowCaseChangingInput(self.tagCombo) form.addRow(_('Tag:'), self.tagCombo) self.tagRevLabel = QLabel('') form.addRow(_('Tagged:'), self.tagRevLabel) ### options expander = qtlib.ExpanderLabel(_('Options'), False) expander.expanded.connect(self.show_options) box.addWidget(expander) optbox = QVBoxLayout() optbox.setSpacing(6) box.addLayout(optbox) hbox = QHBoxLayout() hbox.setSpacing(0) optbox.addLayout(hbox) self.localCheckBox = QCheckBox(_('Local tag')) self.localCheckBox.toggled.connect(self.updateStates) self.replaceCheckBox = QCheckBox(_('Replace existing tag (-f/--force)')) self.replaceCheckBox.toggled.connect(self.updateStates) optbox.addWidget(self.localCheckBox) optbox.addWidget(self.replaceCheckBox) self.englishCheckBox = QCheckBox(_('Use English commit message')) engmsg = repo.ui.configbool('tortoisehg', 'engmsg', False) self.englishCheckBox.setChecked(engmsg) optbox.addWidget(self.englishCheckBox) self.customCheckBox = QCheckBox(_('Use custom commit message:')) self.customCheckBox.toggled.connect(self.customMessageToggle) self.customTextLineEdit = QLineEdit() optbox.addWidget(self.customCheckBox) optbox.addWidget(self.customTextLineEdit) ## bottom buttons BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Close) bbox.rejected.connect(self.reject) self.addBtn = bbox.addButton(_('&Add'), BB.ActionRole) self.removeBtn = bbox.addButton(_('&Remove'), BB.ActionRole) box.addWidget(bbox) self.addBtn.clicked.connect(self.onAddTag) self.removeBtn.clicked.connect(self.onRemoveTag) ## horizontal separator self.sep = QFrame() self.sep.setFrameShadow(QFrame.Sunken) self.sep.setFrameShape(QFrame.HLine) base.addWidget(self.sep) ## status line self.status = qtlib.StatusLabel() self.status.setContentsMargins(4, 2, 4, 4) base.addWidget(self.status) self._finishmsg = None repoagent.repositoryChanged.connect(self.refresh) self.customTextLineEdit.setDisabled(True) self.replaceCheckBox.setChecked(bool(opts.get('force'))) self.localCheckBox.setChecked(bool(opts.get('local'))) if not opts.get('local') and opts.get('message'): msg = hglib.tounicode(opts['message']) self.customCheckBox.setChecked(True) self.customTextLineEdit.setText(msg) self.clear_status() self.show_options(False) self.tagCombo.setFocus() self.refresh() @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def refresh(self): """ update display on dialog with recent repo data """ cur = self.tagCombo.currentText() tags = list(self.repo.tags()) tags.sort(reverse=True) self.tagCombo.clear() for tag in tags: if tag in ('tip', 'qbase', 'qtip', 'qparent'): continue self.tagCombo.addItem(hglib.tounicode(tag)) if cur: self.tagCombo.setEditText(cur) else: self.tagCombo.clearEditText() self.updateStates() @pyqtSlot() def updateStates(self): """ update bottom button sensitives based on rev and tag """ tagu = self.tagCombo.currentText() tag = hglib.fromunicode(tagu) # check tag existence if tag: exists = tag in self.repo.tags() if exists: tagtype = self.repo.tagtype(tag) islocal = 'local' == tagtype try: ctx = self.repo[self.repo.tags()[tag]] trev = ctx.rev() thash = str(ctx) except: trev, thash, local = 0, '????????', '' self.localCheckBox.setChecked(islocal) self.localCheckBox.setEnabled(False) local = islocal and _('local') or '' self.tagRevLabel.setText('%d (%s) %s' % (trev, thash, local)) samerev = trev == self.rev else: islocal = self.localCheckBox.isChecked() self.localCheckBox.setEnabled(True) self.tagRevLabel.clear() force = self.replaceCheckBox.isChecked() custom = self.customCheckBox.isChecked() self.addBtn.setEnabled(not exists or (force and not samerev)) if exists and not samerev: self.addBtn.setText(_('Move')) else: self.addBtn.setText(_('Add')) self.removeBtn.setEnabled(exists) self.englishCheckBox.setEnabled(not islocal) self.customCheckBox.setEnabled(not islocal) self.customTextLineEdit.setEnabled(not islocal and custom) else: self.addBtn.setEnabled(False) self.removeBtn.setEnabled(False) self.localCheckBox.setEnabled(False) self.englishCheckBox.setEnabled(False) self.customCheckBox.setEnabled(False) self.customTextLineEdit.setEnabled(False) self.tagRevLabel.clear() def customMessageToggle(self, checked): self.customTextLineEdit.setEnabled(checked) if checked: self.customTextLineEdit.setFocus() def show_options(self, visible): self.localCheckBox.setVisible(visible) self.replaceCheckBox.setVisible(visible) self.englishCheckBox.setVisible(visible) self.customCheckBox.setVisible(visible) self.customTextLineEdit.setVisible(visible) def set_status(self, text, icon): self.status.setShown(True) self.sep.setShown(True) self.status.set_status(text, icon) def clear_status(self): self.status.setHidden(True) self.sep.setHidden(True) def _runTag(self, tagname, **opts): if not self._cmdsession.isFinished(): self.set_status(_('Repository command still running'), False) return self._finishmsg = opts.pop('finishmsg') cmdline = hglib.buildcmdargs('tag', tagname, **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onTagFinished) @pyqtSlot(int) def _onTagFinished(self, ret): if ret == 0: self.set_status(self._finishmsg, True) else: self.set_status(self._cmdsession.errorString(), False) def onAddTag(self): tagu = self.tagCombo.currentText() tag = hglib.fromunicode(tagu) local = self.localCheckBox.isChecked() force = self.replaceCheckBox.isChecked() english = self.englishCheckBox.isChecked() if self.customCheckBox.isChecked() and not local: message = self.customTextLineEdit.text() else: message = None exists = tag in self.repo.tags() if exists and not force: self.set_status(_("Tag '%s' already exists") % tagu, False) return if not local: parents = self.repo.parents() if len(parents) > 1: self.set_status(_('uncommitted merge'), False) return p1 = parents[0] if not force and p1.node() not in self.repo._branchheads: self.set_status(_('not at a branch head (use force)'), False) return if not message: ctx = self.repo[self.rev] if exists: origctx = self.repo[self.repo.tags()[tag]] msgset = keep._('Moved tag %s to changeset %s' \ ' (from changeset %s)') message = (english and msgset['id'] or msgset['str']) \ % (tagu, str(ctx), str(origctx)) else: msgset = keep._('Added tag %s for changeset %s') message = (english and msgset['id'] or msgset['str']) \ % (tagu, str(ctx)) if exists: finishmsg = _("Tag '%s' has been moved") % tagu else: finishmsg = _("Tag '%s' has been added") % tagu user = qtlib.getCurrentUsername(self, self.repo) if not user: return self._runTag(tagu, rev=self.rev, user=hglib.tounicode(user), local=local, force=force, message=message, finishmsg=finishmsg) def onRemoveTag(self): tagu = self.tagCombo.currentText() tag = hglib.fromunicode(tagu) local = self.localCheckBox.isChecked() force = self.replaceCheckBox.isChecked() english = self.englishCheckBox.isChecked() if self.customCheckBox.isChecked() and not local: message = self.customTextLineEdit.text() else: message = None tagtype = self.repo.tagtype(tag) if local: if tagtype != 'local': self.set_status(_("tag '%s' is not a local tag") % tagu, False) return else: if tagtype != 'global': self.set_status(_("tag '%s' is not a global tag") % tagu, False) return parents = self.repo.parents() if len(parents) > 1: self.set_status(_('uncommitted merge'), False) return p1 = parents[0] if not force and p1.node() not in self.repo._branchheads: self.set_status(_('not at a branch head (use force)'), False) return if not message: msgset = keep._('Removed tag %s') message = (english and msgset['id'] or msgset['str']) % tagu finishmsg = _("Tag '%s' has been removed") % tagu self._runTag(tagu, remove=True, local=local, message=message, finishmsg=finishmsg) def reject(self): if not self._cmdsession.isFinished(): self.set_status(_('Repository command still running'), False) return super(TagDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/docklog.py0000644000076400007640000005331112231647662020033 0ustar stevesteve# docklog.py - Log dock widget for the TortoiseHg Workbench # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import glob, os, shlex from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciScintilla from mercurial import commands, util from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, cmdui from tortoisehg.util import hglib class _LogWidgetForConsole(cmdui.LogWidget): """Wrapped LogWidget for ConsoleWidget""" returnPressed = pyqtSignal(unicode) """Return key pressed when cursor is on prompt line""" historyRequested = pyqtSignal(unicode, int) # keyword, direction completeRequested = pyqtSignal(unicode) _prompt = '% ' def __init__(self, parent=None): super(_LogWidgetForConsole, self).__init__(parent) self._prompt_marker = self.markerDefine(QsciScintilla.Background) self.setMarkerBackgroundColor(QColor('#e8f3fe'), self._prompt_marker) self.cursorPositionChanged.connect(self._updatePrompt) # ensure not moving prompt line even if completion list get shorter, # by allowing to scroll one page below the last line self.SendScintilla(QsciScintilla.SCI_SETENDATLASTLINE, False) # don't reserve "slop" area at top/bottom edge on ensureFooVisible() self.SendScintilla(QsciScintilla.SCI_SETVISIBLEPOLICY, 0, 0) self._savedcommands = [] # temporarily-invisible command self._origcolor = None self._flashtimer = QTimer(self, interval=100, singleShot=True) self._flashtimer.timeout.connect(self._restoreColor) def keyPressEvent(self, event): cursoronprompt = not self.isReadOnly() if cursoronprompt: if event.key() == Qt.Key_Up: return self.historyRequested.emit(self.commandText(), -1) elif event.key() == Qt.Key_Down: return self.historyRequested.emit(self.commandText(), +1) del self._savedcommands[:] # settle candidate by user input if event.key() in (Qt.Key_Return, Qt.Key_Enter): return self.returnPressed.emit(self.commandText()) if event.key() == Qt.Key_Tab: return self.completeRequested.emit(self.commandText()) if event.key() == Qt.Key_Escape: # When ESC is pressed, if the cursor is on the prompt, # this clears it, if not, this moves the cursor to the prompt self.setCommandText('') super(_LogWidgetForConsole, self).keyPressEvent(event) def setPrompt(self, text): if text == self._prompt: return if self._findPromptLine() < 0: self._prompt = text return self.clearPrompt() self._prompt = text self.openPrompt() @pyqtSlot() def openPrompt(self): """Show prompt line and enable user input""" self.closePrompt() line = self.lines() - 1 self.markerAdd(line, self._prompt_marker) self.append(self._prompt) if self._savedcommands: self.append(self._savedcommands.pop()) self.setCursorPosition(line, len(self.text(line))) self.setReadOnly(False) # make sure the prompt line is visible. Because QsciScintilla may # delay line wrapping, setCursorPosition() doesn't always scrolls # to the correct position. # http://www.scintilla.org/ScintillaDoc.html#LineWrapping self.SCN_PAINTED.connect(self._scrollCaretOnPainted) @pyqtSlot() def _scrollCaretOnPainted(self): self.SCN_PAINTED.disconnect(self._scrollCaretOnPainted) self.SendScintilla(self.SCI_SCROLLCARET) def _removeTrailingText(self, line, index): visline = self.firstVisibleLine() lastline = self.lines() - 1 self.setSelection(line, index, lastline, len(self.text(lastline))) self.removeSelectedText() # restore scroll position changed by setSelection() self.verticalScrollBar().setValue(visline) def _findPromptLine(self): return self.markerFindPrevious(self.lines() - 1, 1 << self._prompt_marker) @pyqtSlot() def clearLog(self): wasopen = self._findPromptLine() >= 0 self.clear() if wasopen: self.openPrompt() @pyqtSlot() def closePrompt(self): """Disable user input""" line = self._findPromptLine() if line >= 0: if self.commandText(): self._setmarker((line,), 'control') self.markerDelete(line, self._prompt_marker) self._removeTrailingText(line + 1, 0) # clear completion self._newline() self.setCursorPosition(self.lines() - 1, 0) self.setReadOnly(True) @pyqtSlot() def clearPrompt(self): """Clear prompt line and subsequent text""" line = self._findPromptLine() if line < 0: return self._savedcommands = [self.commandText()] self.markerDelete(line) self._removeTrailingText(line, 0) @pyqtSlot(int, int) def _updatePrompt(self, line, pos): """Update availability of user input""" if self.markersAtLine(line) & (1 << self._prompt_marker): self.setReadOnly(pos < len(self._prompt)) self._ensurePrompt(line) if pos < len(self._prompt): # avoid inconsistency caused by changing pos inside # cursorPositionChanged QTimer.singleShot(0, self._moveCursorToPromptHome) else: self.setReadOnly(True) @pyqtSlot() def _moveCursorToPromptHome(self): line = self._findPromptLine() if line >= 0: self.setCursorPosition(line, len(self._prompt)) def _ensurePrompt(self, line): """Insert prompt string if not available""" s = unicode(self.text(line)) if s.startswith(self._prompt): return for i, c in enumerate(self._prompt): if s[i:i + 1] != c: self.insertAt(self._prompt[i:], line, i) break def commandText(self): """Return the current command text""" if self._savedcommands: return self._savedcommands[-1] l = self._findPromptLine() if l >= 0: return unicode(self.text(l))[len(self._prompt):].rstrip('\n') else: return '' def setCommandText(self, text, candidate=False): """Replace the current command text; subsequent text is also removed. If candidate, the specified text is displayed but does not replace commandText() until the user takes some action. """ line = self._findPromptLine() if line < 0: return if candidate: self._savedcommands = [self.commandText()] else: del self._savedcommands[:] self._ensurePrompt(line) self._removeTrailingText(line, len(self._prompt)) self.insert(text) self.setCursorPosition(line, len(self.text(line))) def _newline(self): if self.text(self.lines() - 1): self.append('\n') def flash(self, color='brown'): """Briefly change the text color to catch the user attention""" if self._flashtimer.isActive(): return self._origcolor = self.color() self.setColor(QColor(color)) self._flashtimer.start() @pyqtSlot() def _restoreColor(self): assert self._origcolor self.setColor(self._origcolor) def _searchhistory(items, text, direction, idx): """Search history items and return (item, index_of_item) Valid index is zero or negative integer. Zero is reserved for non-history item. >>> def searchall(items, text, direction, idx=0): ... matched = [] ... while True: ... it, idx = _searchhistory(items, text, direction, idx) ... if not it: ... return matched, idx ... matched.append(it) >>> searchall('foo bar baz'.split(), '', direction=-1) (['baz', 'bar', 'foo'], -4) >>> searchall('foo bar baz'.split(), '', direction=+1, idx=-3) (['bar', 'baz'], 0) search by keyword: >>> searchall('foo bar baz'.split(), 'b', direction=-1) (['baz', 'bar'], -4) >>> searchall('foo bar baz'.split(), 'inexistent', direction=-1) ([], -4) empty history: >>> searchall([], '', direction=-1) ([], -1) initial index out of range: >>> searchall('foo bar baz'.split(), '', direction=-1, idx=-3) ([], -4) >>> searchall('foo bar baz'.split(), '', direction=+1, idx=0) ([], 1) """ assert direction != 0 idx += direction while -len(items) <= idx < 0: curcmdline = items[idx] if curcmdline.startswith(text): return curcmdline, idx idx += direction return None, idx class ConsoleWidget(QWidget): """Console to run hg/thg command and show output""" closeRequested = pyqtSignal() progressReceived = pyqtSignal(QString, object, QString, QString, object, object) """Emitted when progress received Args: topic, pos, item, unit, total, reporoot """ def __init__(self, agent, parent=None): super(ConsoleWidget, self).__init__(parent) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self._initlogwidget() self.setFocusProxy(self._logwidget) self._agent = agent agent.busyChanged.connect(self._suppressPromptOnBusy) agent.outputReceived.connect(self._logwidget.appendLog) agent.progressReceived.connect(self._emitProgress) if self._repo: self._logwidget.setPrompt('%s%% ' % self._repo.displayname) self.openPrompt() self._commandHistory = [] self._commandIdx = 0 def _initlogwidget(self): self._logwidget = _LogWidgetForConsole(self) self._logwidget.returnPressed.connect(self._runcommand) self._logwidget.historyRequested.connect(self.historySearch) self._logwidget.completeRequested.connect(self.completeCommandText) self.layout().addWidget(self._logwidget) # compatibility methods with LogWidget for name in ('openPrompt', 'closePrompt', 'clear'): setattr(self, name, getattr(self._logwidget, name)) @pyqtSlot(unicode, int) def historySearch(self, text, direction): cmdline, idx = _searchhistory(self._commandHistory, unicode(text), direction, self._commandIdx) if cmdline: self._commandIdx = idx self._logwidget.setCommandText(cmdline, candidate=True) else: self._logwidget.flash() def _commandComplete(self, cmdtype, cmdline): from tortoisehg.hgqt import run matches = [] cmd = cmdline.split() if cmdtype == 'hg': cmdtable = commands.table else: cmdtable = run.table subcmd = '' if len(cmd) >= 2: subcmd = cmd[1].lower() def findhgcmd(cmdstart): matchinfo = {} for cmdspec in cmdtable: for cmdname in cmdspec.split('|'): if cmdname[0] == '^': cmdname = cmdname[1:] if cmdname.startswith(cmdstart): matchinfo[cmdname] = cmdspec return matchinfo matchingcmds = findhgcmd(subcmd) if not matchingcmds: return matches if len(matchingcmds) > 1: basecmdline = '%s %%s' % (cmdtype) matches = [basecmdline % c for c in matchingcmds] else: scmdtype = matchingcmds.keys()[0] cmdspec = matchingcmds[scmdtype] opts = cmdtable[cmdspec][1] def findcmdopt(cmdopt): cmdopt = cmdopt.lower() while(cmdopt.startswith('-')): cmdopt = cmdopt[1:] matchingopts = [] for opt in opts: if opt[1].startswith(cmdopt): matchingopts.append(opt) return matchingopts basecmdline = '%s %s --%%s' % (cmdtype, scmdtype) if len(cmd) == 2: matches = ['%s %s ' % (cmdtype, scmdtype)] matches += [basecmdline % opt[1] for opt in opts] else: cmdopt = cmd[-1] if cmdopt.startswith('-'): # find the matching options basecmdline = ' '.join(cmd[:-1]) + ' --%s' cmdopts = findcmdopt(cmdopt) matches = [basecmdline % opt[1] for opt in cmdopts] return sorted(matches) @pyqtSlot(unicode) def completeCommandText(self, text): """Show the list of history or known commands matching the search text Also complete the prompt with the common prefix to the matching items """ text = unicode(text).strip() if not text: self._logwidget.flash() return history = set(self._commandHistory) commonprefix = '' matches = [] for cmdline in history: if cmdline.startswith(text): matches.append(cmdline) if matches: matches.sort() commonprefix = os.path.commonprefix(matches) cmd = text.split() cmdtype = cmd[0].lower() if cmdtype in ('hg', 'thg'): hgcommandmatches = self._commandComplete(cmdtype, text) if hgcommandmatches: if not commonprefix: commonprefix = os.path.commonprefix(hgcommandmatches) if matches: matches.append('------ %s commands ------' % cmdtype) matches += hgcommandmatches if not matches: self._logwidget.flash() return self._logwidget.setCommandText(commonprefix) if len(matches) > 1: self._logwidget.append('\n' + '\n'.join(matches) + '\n') self._logwidget.ensureLineVisible(self._logwidget.lines() - 1) self._logwidget.ensureCursorVisible() @util.propertycache def _extproc(self): extproc = QProcess(self) extproc.started.connect(self.closePrompt) extproc.finished.connect(self.openPrompt) extproc.error.connect(self._handleExtprocError) extproc.readyReadStandardOutput.connect(self._appendExtprocStdout) extproc.readyReadStandardError.connect(self._appendExtprocStderr) return extproc @pyqtSlot() def _handleExtprocError(self): if self._extproc.state() == QProcess.NotRunning: self._logwidget.closePrompt() msg = self._extproc.errorString() self._logwidget.appendLog(msg + '\n', 'ui.error') if self._extproc.state() == QProcess.NotRunning: self._logwidget.openPrompt() @pyqtSlot() def _appendExtprocStdout(self): text = hglib.tounicode(self._extproc.readAllStandardOutput().data()) self._logwidget.appendLog(text, '') @pyqtSlot() def _appendExtprocStderr(self): text = hglib.tounicode(self._extproc.readAllStandardError().data()) self._logwidget.appendLog(text, 'ui.error') @pyqtSlot(unicode, str) def appendLog(self, msg, label): """Append log text from another cmdui""" self._logwidget.clearPrompt() try: self._logwidget.appendLog(msg, label) finally: if not self._agent.isBusy(): self.openPrompt() def repoRootPath(self): if util.safehasattr(self._agent, 'rootPath'): return self._agent.rootPath() @property def _repo(self): if util.safehasattr(self._agent, 'rawRepo'): return self._agent.rawRepo() def _workingDirectory(self): return self.repoRootPath() or os.getcwdu() @pyqtSlot(bool) def _suppressPromptOnBusy(self, busy): if busy: self._logwidget.clearPrompt() else: self.openPrompt() @pyqtSlot(unicode, object, unicode, unicode, object) def _emitProgress(self, *args): self.progressReceived.emit( *(args + (self._repo and self._repo.root or None,))) @pyqtSlot(unicode) def _runcommand(self, cmdline): cmdline = unicode(cmdline) self._commandIdx = 0 try: args = list(self._parsecmdline(cmdline)) except ValueError, e: self.closePrompt() self._logwidget.appendLog(unicode(e) + '\n', 'ui.error') self.openPrompt() return if not args: self.openPrompt() return # add command to command history if not self._commandHistory or self._commandHistory[-1] != cmdline: self._commandHistory.append(cmdline) # execute the command cmd = args.pop(0) try: self._cmdtable[cmd](self, args) except KeyError: return self._runextcommand(cmdline) def _parsecmdline(self, cmdline): """Split command line string to imitate a unix shell""" try: # shlex can't process unicode on Python < 2.7.3 args = shlex.split(cmdline.encode('utf-8')) except ValueError, e: raise ValueError(_('command parse error: %s') % e) for e in args: e = util.expandpath(e).decode('utf-8') if util.any(c in e for c in '*?[]'): expanded = glob.glob(os.path.join(self._workingDirectory(), e)) if not expanded: raise ValueError(_('no matches found: %s') % e) for p in expanded: yield p else: yield e def _runextcommand(self, cmdline): self._extproc.setWorkingDirectory(self._workingDirectory()) self._extproc.start(cmdline, QIODevice.ReadOnly) def _cmd_hg(self, args): self.closePrompt() self._agent.runCommand(args) def _cmd_thg(self, args): from tortoisehg.hgqt import run self.closePrompt() try: if self.repoRootPath(): args = ['-R', self.repoRootPath()] + args # TODO: show errors run.dispatch(map(hglib.fromunicode, args)) finally: self.openPrompt() def _cmd_clear(self, args): self._logwidget.clearLog() def _cmd_exit(self, args): self._logwidget.clearLog() self.closeRequested.emit() _cmdtable = { 'hg': _cmd_hg, 'thg': _cmd_thg, 'clear': _cmd_clear, 'cls': _cmd_clear, 'exit': _cmd_exit, } class LogDockWidget(QDockWidget): progressReceived = pyqtSignal(QString, object, QString, QString, object, object) def __init__(self, repomanager, parent=None): super(LogDockWidget, self).__init__(parent) self.setFeatures(QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.setWindowTitle(_('Output Log')) # Not enabled until we have a way to make it configurable #self.setWindowFlags(Qt.Drawer) self.dockLocationChanged.connect(self._updateTitleBarStyle) self._repomanager = repomanager self._repomanager.repositoryOpened.connect(self._createConsoleFor) self._repomanager.repositoryClosed.connect(self._destroyConsoleFor) self._consoles = QStackedWidget(self) self.setWidget(self._consoles) self._createConsole(cmdcore.CmdAgent(self)) for root in self._repomanager.repoRootPaths(): self._createConsoleFor(root) # move focus only when console is activated by keyboard/mouse operation self.toggleViewAction().triggered.connect(self._setFocusOnToggleView) def setCurrentRepoRoot(self, root): w = self._findConsoleFor(root) self._consoles.setCurrentWidget(w) def _findConsoleFor(self, root): for i in xrange(self._consoles.count()): w = self._consoles.widget(i) if w.repoRootPath() == root: return w raise ValueError('no console found for %r' % root) def _createConsole(self, agent): w = ConsoleWidget(agent, self) w.closeRequested.connect(self.close) w.progressReceived.connect(self.progressReceived) self._consoles.addWidget(w) return w @pyqtSlot(unicode) def _createConsoleFor(self, root): root = unicode(root) repoagent = self._repomanager.repoAgent(root) assert repoagent self._createConsole(repoagent) @pyqtSlot(unicode) def _destroyConsoleFor(self, root): root = unicode(root) w = self._findConsoleFor(root) self._consoles.removeWidget(w) w.setParent(None) @pyqtSlot() def clear(self): w = self._consoles.currentWidget() w.clear() def appendLog(self, msg, label, reporoot=None): w = self._findConsoleFor(reporoot) w.appendLog(msg, label) @pyqtSlot(bool) def _setFocusOnToggleView(self, visible): if visible: w = self._consoles.currentWidget() w.setFocus() def setVisible(self, visible): super(LogDockWidget, self).setVisible(visible) if visible: self.raise_() @pyqtSlot(Qt.DockWidgetArea) def _updateTitleBarStyle(self, area): f = self.features() if area & (Qt.TopDockWidgetArea | Qt.BottomDockWidgetArea): f |= QDockWidget.DockWidgetVerticalTitleBar # saves vertical space else: f &= ~QDockWidget.DockWidgetVerticalTitleBar self.setFeatures(f) tortoisehg-2.10/tortoisehg/hgqt/repomodel.py0000644000076400007640000007401212231647662020400 0ustar stevesteve# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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 binascii, os, re from mercurial import util, error from mercurial.util import propertycache from mercurial.context import workingctx from tortoisehg.util import hglib from tortoisehg.hgqt.graph import Graph from tortoisehg.hgqt.graph import revision_grapher from tortoisehg.hgqt.graph import LINE_TYPE_GRAFT from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import * from PyQt4.QtGui import * nullvariant = QVariant() mqpatchmimetype = 'application/thg-mqunappliedpatch' # TODO: Remove these two when we adopt GTK author color scheme COLORS = [ "blue", "darkgreen", "red", "green", "darkblue", "purple", "dodgerblue", Qt.darkYellow, "magenta", "darkred", "darkmagenta", "darkcyan", "gray", ] COLORS = [str(QColor(x).name()) for x in COLORS] COLUMNHEADERS = ( ('Graph', _('Graph', 'column header')), ('Rev', _('Rev', 'column header')), ('Branch', _('Branch', 'column header')), ('Description', _('Description', 'column header')), ('Author', _('Author', 'column header')), ('Tags', _('Tags', 'column header')), ('Latest tags', _('Latest tags', 'column header')), ('Node', _('Node', 'column header')), ('Age', _('Age', 'column header')), ('LocalTime', _('Local Time', 'column header')), ('UTCTime', _('UTC Time', 'column header')), ('Changes', _('Changes', 'column header')), ('Converted', _('Converted From', 'column header')), ('Phase', _('Phase', 'column header')), ) UNAPPLIED_PATCH_COLOR = '#999999' HIDDENREV_COLOR = '#666666' GraphRole = Qt.UserRole + 0 def get_color(n, ignore=()): """ Return a color at index 'n' rotating in the available colors. 'ignore' is a list of colors not to be chosen. """ ignore = [str(QColor(x).name()) for x in ignore] colors = [x for x in COLORS if x not in ignore] if not colors: # ghh, no more available colors... colors = COLORS return colors[n % len(colors)] def get_style(line_type, active): if line_type == LINE_TYPE_GRAFT: return Qt.DashLine return Qt.SolidLine def get_width(line_type, active): if line_type == LINE_TYPE_GRAFT or not active: return 1 return 2 def _parsebranchcolors(value): r"""Parse tortoisehg.branchcolors setting >>> _parsebranchcolors('foo:#123456 bar:#789abc ') [('foo', '#123456'), ('bar', '#789abc')] >>> _parsebranchcolors(r'foo\ bar:black foo\:bar:white') [('foo bar', 'black'), ('foo:bar', 'white')] >>> _parsebranchcolors(r'\u00c0:black') [('\xc0', 'black')] >>> _parsebranchcolors('\xc0:black') [('\xc0', 'black')] >>> _parsebranchcolors(None) [] >>> _parsebranchcolors('ill:formed:value no-value') [] >>> _parsebranchcolors(r'\ubad:unicode-repr') [] """ if not value: return [] colors = [] for e in re.split(r'(?:(?<=\\\\)|(? (n - self.fill_step / 2): required = row - n + self.fill_step if required or buildrev: self.graph.build_nodes(nnodes=required, rev=buildrev) self.updateRowCount() if self.rowcount >= len(self.graph): return # no need to update row count if row and row > self.rowcount: # asked row was already built, but views where not aware of this self.updateRowCount() elif rev is not None and rev <= self.graph[self.rowcount].rev: # asked rev was already built, but views where not aware of this self.updateRowCount() def loadall(self): self.timerHandle = self.startTimer(1) def timerEvent(self, event): if event.timerId() == self.timerHandle: self.showMessage.emit(_('filling (%d)')%(len(self.graph))) if self.graph.isfilled(): self.killTimer(self.timerHandle) self.timerHandle = None self.showMessage.emit('') self.loaded.emit() # we only fill the graph data structures without telling # views until the model is loaded, to keep maximal GUI # reactivity elif not self.graph.build_nodes(): self.killTimer(self.timerHandle) self.timerHandle = None self.updateRowCount() self.showMessage.emit('') self.loaded.emit() def updateRowCount(self): currentlen = self.rowcount newlen = len(self.graph) if newlen > self.rowcount: self.beginInsertRows(QModelIndex(), currentlen, newlen-1) self.rowcount = newlen self.endInsertRows() def rowCount(self, parent): if parent.isValid(): return 0 return self.rowcount def columnCount(self, parent): if parent.isValid(): return 0 return len(self._columns) def maxWidthValueForColumn(self, col): if self.graph is None: return 'XXXX' column = self._columns[col] if column == 'Rev': return '8' * len(str(len(self.repo))) + '+' if column == 'Node': return '8' * 12 + '+' if column in ('LocalTime', 'UTCTime'): return hglib.displaytime(util.makedate()) if column in ('Tags', 'Latest tags'): try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == 'Branch': try: return sorted(self.repo.branchtags().keys(), key=lambda x: len(x))[-1] except IndexError: pass if column == 'Filename': return self.filename if column == 'Graph': res = self.col2x(self.graph.max_cols) return min(res, 150) if column == 'Changes': return 'Changes' # Fall through for Description return None def user_color(self, user): 'deprecated, please replace with hgtk color scheme' if user not in self._user_colors: self._user_colors[user] = get_color(len(self._user_colors), self._user_colors.values()) return self._user_colors[user] def namedbranch_color(self, branch): 'deprecated, please replace with hgtk color scheme' if branch not in self._branch_colors: self._branch_colors[branch] = get_color(len(self._branch_colors)) return self._branch_colors[branch] def col2x(self, col): return 2 * self.dotradius * col + self.dotradius/2 + 8 def graphctx(self, ctx, gnode): w = self.col2x(gnode.cols) + 10 h = self.rowheight pix = QPixmap(w, h) pix.fill(QColor(0,0,0,0)) painter = QPainter(pix) try: self._drawgraphctx(painter, pix, ctx, gnode) finally: painter.end() return QVariant(pix) def _drawgraphctx(self, painter, pix, ctx, gnode): revset = self.revset h = pix.height() dot_y = h / 2 painter.setRenderHint(QPainter.Antialiasing) pen = QPen(Qt.blue) pen.setWidth(2) painter.setPen(pen) lpen = QPen(pen) lpen.setColor(Qt.black) painter.setPen(lpen) if revset: def isactive(start, end, color, line_type, children, rev): return rev in revset and util.any(r in revset for r in children) else: def isactive(start, end, color, line_type, children, rev): return True for y1, y4, lines in ((dot_y, dot_y + h, gnode.bottomlines), (dot_y - h, dot_y, gnode.toplines)): y2 = y1 + 1 * (y4 - y1)/4 ymid = (y1 + y4)/2 y3 = y1 + 3 * (y4 - y1)/4 lines = sorted((isactive(*l), l) for l in lines) for active, (start, end, color, line_type, children, rev) in lines: lpen = QPen(pen) lpen.setColor(QColor(active and get_color(color) or "gray")) lpen.setStyle(get_style(line_type, active)) lpen.setWidth(get_width(line_type, active)) painter.setPen(lpen) x1 = self.col2x(start) x2 = self.col2x(end) path = QPainterPath() path.moveTo(x1, y1) path.cubicTo(x1, y2, x1, y2, (x1 + x2)/2, ymid) path.cubicTo(x2, y3, x2, y3, x2, y4) painter.drawPath(path) # Draw node if revset and gnode.rev not in revset: dot_color = QColor("gray") radius = self.dotradius * 0.8 else: dot_color = QColor(self.namedbranch_color(ctx.branch())) radius = self.dotradius dotcolor = dot_color.lighter() pencolor = dot_color.darker() truewhite = QColor("white") white = QColor("white") fillcolor = gnode.rev is None and white or dotcolor pen = QPen(pencolor) pen.setWidthF(1.5) painter.setPen(pen) centre_x = self.col2x(gnode.x) centre_y = h/2 def circle(r): rect = QRectF(centre_x - r, centre_y - r, 2 * r, 2 * r) painter.drawEllipse(rect) def closesymbol(s): rect_ = QRectF(centre_x - 1.5 * s, centre_y - 0.5 * s, 3 * s, s) painter.drawRect(rect_) def diamond(r): poly = QPolygonF([QPointF(centre_x - r, centre_y), QPointF(centre_x, centre_y - r), QPointF(centre_x + r, centre_y), QPointF(centre_x, centre_y + r), QPointF(centre_x - r, centre_y),]) painter.drawPolygon(poly) hiddenrev = ctx.hidden() if hiddenrev: painter.setBrush(truewhite) white.setAlpha(64) fillcolor.setAlpha(64) if ctx.thgmqappliedpatch(): # diamonds for patches symbolsize = radius / 1.5 if hiddenrev: diamond(symbolsize) if ctx.thgwdparent(): painter.setBrush(white) diamond(2 * 0.9 * symbolsize) painter.setBrush(fillcolor) diamond(symbolsize) elif ctx.thgmqunappliedpatch(): symbolsize = radius / 1.5 if hiddenrev: diamond(symbolsize) patchcolor = QColor('#dddddd') painter.setBrush(patchcolor) painter.setPen(patchcolor) diamond(symbolsize) elif ctx.extra().get('close'): symbolsize = 0.5 * radius if hiddenrev: closesymbol(symbolsize) painter.setBrush(fillcolor) closesymbol(symbolsize) else: # circles for normal revisions symbolsize = 0.5 * radius if hiddenrev: circle(symbolsize) if ctx.thgwdparent(): painter.setBrush(white) circle(0.9 * radius) painter.setBrush(fillcolor) circle(symbolsize) def invalidateCache(self): self._cache = [] for a in ('_roleoffsets',): if hasattr(self, a): delattr(self, a) @propertycache def _roleoffsets(self): return {Qt.DisplayRole : 0, Qt.ForegroundRole : len(self._columns), GraphRole : len(self._columns) * 2} def data(self, index, role): if not index.isValid(): return nullvariant # font is not cached in self._cache since it is equal for all rows if role == Qt.FontRole: column = self._columns[index.column()] return self._columnfonts.get(column, nullvariant) if role not in self._roleoffsets: return nullvariant # repo may be changed while reading in case of postpull=rebase for # example, and result in RevlogError. (issue #429) try: return self.safedata(index, role) except error.RevlogError, e: if 'THGDEBUG' in os.environ: raise if role == Qt.DisplayRole: return QVariant(hglib.tounicode(str(e))) else: return nullvariant def safedata(self, index, role): row = index.row() self.ensureBuilt(row=row) graphlen = len(self.graph) cachelen = len(self._cache) if graphlen > cachelen: self._cache.extend([None,] * (graphlen-cachelen)) data = self._cache[row] if data is None: data = [None,] * (self._roleoffsets[GraphRole]+1) column = self._columns[index.column()] offset = self._roleoffsets[role] if role == GraphRole: if column != 'Graph': return nullvariant if data[offset] is None: gnode = self.graph[row] ctx = self.repo.changectx(gnode.rev) data[offset] = self.graphctx(ctx, gnode) self._cache[row] = data return data[offset] else: idx = index.column() + offset if data[idx] is None: try: result = self.rawdata(row, column, role) except util.Abort: result = nullvariant data[idx] = result self._cache[row] = data return data[idx] def rawdata(self, row, column, role): gnode = self.graph[row] ctx = self.repo.changectx(gnode.rev) if role == Qt.DisplayRole: text = self._columnmap[column](self, ctx, gnode) if not isinstance(text, (QString, unicode)): text = hglib.tounicode(text) return QVariant(text) elif role == Qt.ForegroundRole: if ctx.thgmqunappliedpatch(): return QColor(UNAPPLIED_PATCH_COLOR) if ctx.hidden(): return QColor(HIDDENREV_COLOR) if column == 'Author': if self.authorcolor: return QVariant(QColor(self.user_color(ctx.user()))) return nullvariant if column == 'Branch': return QVariant(QColor(self.namedbranch_color(ctx.branch()))) return nullvariant def flags(self, index): if not index.isValid(): return Qt.ItemFlags(0) row = index.row() self.ensureBuilt(row=row) if row >= len(self.graph): return Qt.ItemFlags(0) gnode = self.graph[row] ctx = self.repo.changectx(gnode.rev) dragflags = Qt.ItemFlags(0) if ctx.thgmqunappliedpatch(): dragflags = Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled if isinstance(ctx, workingctx): dragflags |= Qt.ItemIsDropEnabled if not self.revset: return Qt.ItemIsSelectable | Qt.ItemIsEnabled | dragflags if ctx.rev() not in self.revset: return Qt.ItemFlags(0) return Qt.ItemIsSelectable | Qt.ItemIsEnabled | dragflags def mimeTypes(self): return QStringList([mqpatchmimetype]) def supportedDropActions(self): return Qt.MoveAction def mimeData(self, indexes): data = set() for index in indexes: row = str(index.row()) if row not in data: data.add(row) qmd = QMimeData() bytearray = QByteArray(','.join(sorted(data, reverse=True))) qmd.setData(mqpatchmimetype, bytearray) return qmd def dropMimeData(self, data, action, row, column, parent): if mqpatchmimetype not in data.formats(): return False dragrows = [int(r) for r in str(data.data(mqpatchmimetype)).split(',')] destrow = parent.row() if destrow < 0: return False unapplied = self.repo.thgmqunappliedpatches[::-1] applied = [p.name for p in self.repo.mq.applied[::-1]] if max(dragrows) >= len(unapplied): return False dragpatches = [unapplied[d] for d in dragrows] allpatches = unapplied + applied if destrow < len(allpatches): destpatch = allpatches[destrow] else: destpatch = None # next to working rev self.repo.incrementBusyCount() try: hglib.movemqpatches(self.repo, destpatch, dragpatches) finally: self.repo.decrementBusyCount() return True def headerData(self, section, orientation, role): if orientation == Qt.Horizontal: if role == Qt.DisplayRole: return QVariant(self._allcolnames[self._columns[section]]) return nullvariant def rowFromRev(self, rev): row = self.graph.index(rev) if row == -1: row = None return row def indexFromRev(self, rev): if self.graph is None: return None self.ensureBuilt(rev=rev) row = self.rowFromRev(rev) if row is not None: return self.index(row, 0) return None def clear(self): 'empty the list' self.graph = None self.datacache = {} self.layoutChanged.emit() def getbranch(self, ctx, gnode): b = hglib.tounicode(ctx.branch()) if ctx.extra().get('close'): if self.unicodexinabox: b += u' \u2327' else: b += u'--' return b def getlatesttags(self, ctx, gnode): rev = ctx.rev() todo = [rev] repo = self.repo while todo: rev = todo.pop() if rev in self.latesttags: continue ctx = repo[rev] tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global'] if tags: self.latesttags[rev] = ':'.join(sorted(tags)) continue try: if (ctx.parents()): ptag = max( self.latesttags[p.rev()] for p in ctx.parents()) else: ptag = "" except KeyError: # Cache miss - recurse todo.append(rev) todo.extend(p.rev() for p in ctx.parents()) continue self.latesttags[rev] = ptag return self.latesttags[rev] def gettags(self, ctx, gnode): if ctx.rev() is None: return '' tags = [t for t in ctx.tags() if t not in self._mqtags] return hglib.tounicode(','.join(tags)) def getrev(self, ctx, gnode): rev = ctx.rev() if type(rev) is int: return str(rev) elif rev is None: return u'%d+' % ctx.p1().rev() else: return '' def getauthor(self, ctx, gnode): try: user = ctx.user() if not self.fullauthorname: user = hglib.username(user) return user except error.Abort: return _('Mercurial User') def getlog(self, ctx, gnode): if ctx.rev() is None: msg = None if self.unicodestar: # The Unicode symbol is a black star: msg = u'\u2605 ' + _('Working Directory') + u' \u2605' else: msg = '*** ' + _('Working Directory') + ' ***' for pctx in ctx.parents(): if self.repo._branchheads and pctx.node() not in self.repo._branchheads: text = _('Not a head revision!') msg += " " + qtlib.markup(text, fg='red', weight='bold') return msg msg = ctx.longsummary() if ctx.thgmqunappliedpatch(): effects = qtlib.geteffect('log.unapplied_patch') text = qtlib.applyeffects(' %s ' % ctx._patchname, effects) # qtlib.markup(msg, fg=UNAPPLIED_PATCH_COLOR) msg = qtlib.markup(msg) return hglib.tounicode(text + ' ') + msg if ctx.hidden(): return qtlib.markup(msg, fg=HIDDENREV_COLOR) parts = [] if ctx.thgbranchhead(): branchu = hglib.tounicode(ctx.branch()) effects = qtlib.geteffect('log.branch') parts.append(qtlib.applyeffects(u' %s ' % branchu, effects)) for mark in ctx.bookmarks(): style = 'log.bookmark' if mark == self.repo._bookmarkcurrent: bn = self.repo._bookmarks[self.repo._bookmarkcurrent] if bn in self.repo.dirstate.parents(): style = 'log.curbookmark' marku = hglib.tounicode(mark) effects = qtlib.geteffect(style) parts.append(qtlib.applyeffects(u' %s ' % marku, effects)) for tag in ctx.thgtags(): if self.repo.thgmqtag(tag): style = 'log.patch' else: style = 'log.tag' tagu = hglib.tounicode(tag) effects = qtlib.geteffect(style) parts.append(qtlib.applyeffects(u' %s ' % tagu, effects)) if msg: if ctx.thgwdparent(): msg = qtlib.markup(msg, weight='bold') else: msg = qtlib.markup(msg) parts.append(hglib.tounicode(msg)) return ' '.join(parts) def getchanges(self, ctx, gnode): """Return the MAR status for the given ctx.""" changes = [] M, A, R = ctx.changesToParent(0) def addtotal(files, style): effects = qtlib.geteffect(style) text = qtlib.applyeffects(' %s ' % len(files), effects) changes.append(text) if A: addtotal(A, 'log.added') if M: addtotal(M, 'log.modified') if R: addtotal(R, 'log.removed') return ''.join(changes) def getconv(self, ctx, gnode): if ctx.rev() is not None: extra = ctx.extra() cvt = extra.get('convert_revision', '') if cvt: if cvt.startswith('svn:'): return cvt.split('@')[-1] if len(cvt) == 40: try: binascii.unhexlify(cvt) return cvt[:12] except TypeError: pass cvt = extra.get('p4', '') if cvt: return cvt return '' def getphase(self, ctx, gnode): if ctx.rev() is None: return '' try: return ctx.phasestr() except: return 'draft' _columnmap = { 'Rev': getrev, 'Node': lambda self, ctx, gnode: str(ctx), 'Graph': lambda self, ctx, gnode: "", 'Description': getlog, 'Author': getauthor, 'Tags': gettags, 'Latest tags': getlatesttags, 'Branch': getbranch, 'Filename': lambda self, ctx, gnode: gnode.extra[0], 'Age': lambda self, ctx, gnode: hglib.age(ctx.date()).decode('utf-8'), 'LocalTime':lambda self, ctx, gnode: hglib.displaytime(ctx.date()), 'UTCTime': lambda self, ctx, gnode: hglib.utctime(ctx.date()), 'Changes': getchanges, 'Converted': getconv, 'Phase': getphase, } tortoisehg-2.10/tortoisehg/hgqt/messageentry.py0000644000076400007640000002272212231647662021121 0ustar stevesteve# messageentry.py - TortoiseHg's commit message editng widget # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciScintilla, QsciLexerMakefile from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, qscilib import re class MessageEntry(qscilib.Scintilla): def __init__(self, parent, getCheckedFunc=None): super(MessageEntry, self).__init__(parent) self.setEdgeColor(QColor('LightSalmon')) self.setEdgeMode(QsciScintilla.EdgeLine) self.setReadOnly(False) self.setMarginWidth(1, 0) self.setFont(qtlib.getfont('fontcomment').font()) self.setCaretWidth(10) self.setCaretLineBackgroundColor(QColor("#e6fff0")) self.setCaretLineVisible(True) self.setAutoIndent(True) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.setAutoCompletionFillupsEnabled(True) self.setMatchedBraceBackgroundColor(Qt.yellow) self.setIndentationsUseTabs(False) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # http://www.riverbankcomputing.com/pipermail/qscintilla/2009-February/000461.html self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # default message entry widgets to word wrap, user may override self.setWrapMode(QsciScintilla.WrapWord) self.getChecked = getCheckedFunc self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.menuRequested) self.applylexer() self._re_boundary = re.compile('[0-9i#]+\.|\(?[0-9i#]+\)|\(@\)') def setText(self, text): result = super(MessageEntry, self).setText(text) self.setDefaultEolMode() return result def applylexer(self): font = qtlib.getfont('fontcomment').font() self.fontHeight = QFontMetrics(font).height() if QSettings().value('msgentry/lexer', True).toBool(): self.setLexer(QsciLexerMakefile(self)) self.lexer().setColor(QColor(Qt.red), QsciLexerMakefile.Error) self.lexer().setFont(font) else: self.setLexer(None) self.setFont(font) @pyqtSlot(QPoint) def menuRequested(self, point): menu = self._createContextMenu(point) menu.exec_(self.viewport().mapToGlobal(point)) menu.setParent(None) def _createContextMenu(self, point): line = self.lineAt(point) lexerenabled = self.lexer() is not None def apply(): firstline, firstcol, lastline, lastcol = self.getSelection() if firstline < 0: line = 0 else: line = firstline self.beginUndoAction() while True: line = self.reflowBlock(line) if line is None or (line > lastline > -1): break self.endUndoAction() def paste(): files = self.getChecked() self.insert('\n'.join(sorted(files))) def settings(): from tortoisehg.hgqt.settings import SettingsDialog dlg = SettingsDialog(True, focus='tortoisehg.summarylen') dlg.exec_() def togglelexer(): QSettings().setValue('msgentry/lexer', not lexerenabled) self.applylexer() menu = self.createEditorContextMenu() menu.addSeparator() a = menu.addAction(_('Syntax Highlighting')) a.setCheckable(True) a.setChecked(lexerenabled) a.triggered.connect(togglelexer) menu.addSeparator() if self.getChecked: action = menu.addAction(_('Paste &Filenames')) action.triggered.connect(paste) for name, func in [(_('App&ly Format'), apply), (_('C&onfigure Format'), settings)]: def add(name, func): action = menu.addAction(name) action.triggered.connect(func) add(name, func) return menu def refresh(self, repo): self.setEdgeColumn(repo.summarylen) self.setIndentationWidth(repo.tabwidth) self.setTabWidth(repo.tabwidth) self.summarylen = repo.summarylen def reflowBlock(self, line): lines = self.text().split('\n', QString.KeepEmptyParts) if line >= len(lines): return None if not len(lines[line]) > 1: return line+1 # find boundaries (empty lines or bounds) def istopboundary(linetext): # top boundary lines are those that begin with a Markdown style marker # or are empty if not linetext: return True if (linetext[0] in '#-*+'): return True if len(linetext) >= 2: if linetext[:2] in ('> ', '| '): return True if self._re_boundary.match(linetext): return True return False def isbottomboundary(linetext): # bottom boundary lines are those that end with a period # or are empty if not linetext or linetext[-1] == '.': return True return False def isanyboundary(linetext): if len(linetext) >= 3: if linetext[:3] in ('~~~', '```', '---', '==='): return True return False b = line while b and len(lines[b-1]) > 1: linetext = unicode(lines[b].trimmed()) if istopboundary(linetext) or isanyboundary(linetext): break if b >= 1: nextlinetext = unicode(lines[b - 1].trimmed()) if isbottomboundary(nextlinetext) \ or isanyboundary(nextlinetext): break b -= 1 e = line while e+1 < len(lines) and len(lines[e+1]) > 1: linetext = unicode(lines[e].trimmed()) if isbottomboundary(linetext) or isanyboundary(linetext): break nextlinetext = unicode(lines[e+1].trimmed()) if isanyboundary(nextlinetext) or istopboundary(nextlinetext): break e += 1 if b == e == 0: return line + 1 group = [lines[l] for l in xrange(b, e+1)] MARKER = u'\033\033\033\033\033' curlinenum, curcol = self.getCursorPosition() if b <= curlinenum <= e: # insert a "marker" at the cursor position group[curlinenum - b] = \ group[curlinenum - b].insert(curcol, MARKER) firstlinetext = unicode(lines[b]) if firstlinetext: indentcount = len(firstlinetext) - len(firstlinetext.lstrip()) firstindent = firstlinetext[:indentcount] else: indentcount = 0 firstindent = '' group = QStringList([line.simplified() for line in group]) sentence = group.join(' ') parts = sentence.split(' ', QString.SkipEmptyParts) outlines = QStringList() line = QStringList() partslen = indentcount - 1 newcurline, newcurcol = b, 0 for part in parts: if MARKER and MARKER in part: # wherever the marker is found, that is where the cursor # must be moved to after the reflow is done newcurlinenum = b + len(outlines) newcurcol = len(line.join(' ')) + 1 + part.indexOf(MARKER) part = part.replace(MARKER, '') MARKER = None # there is no need to search any more if not part: continue if partslen + len(line) + len(part) + 1 > self.summarylen: if line: linetext = line.join(' ') if len(outlines) == 0 and firstindent: linetext = firstindent + linetext outlines.append(linetext) line, partslen = QStringList(), 0 line.append(part) partslen += len(part) if line: outlines.append(line.join(' ')) self.beginUndoAction() self.setSelection(b, 0, e+1, 0) self.removeSelectedText() self.insertAt(outlines.join('\n')+'\n', b, 0) self.endUndoAction() # restore the cursor position self.setCursorPosition(newcurlinenum, newcurcol) return b + len(outlines) + 1 def moveCursorToEnd(self): lines = self.lines() if lines: lines -= 1 pos = self.lineLength(lines) self.setCursorPosition(lines, pos) self.ensureLineVisible(lines) self.horizontalScrollBar().setSliderPosition(0) def keyPressEvent(self, event): if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_E: line, col = self.getCursorPosition() self.reflowBlock(line) else: super(MessageEntry, self).keyPressEvent(event) def resizeEvent(self, event): super(MessageEntry, self).resizeEvent(event) self.showHScrollBar(self.frameGeometry().height() > self.fontHeight * 3) def minimumSizeHint(self): size = super(MessageEntry, self).minimumSizeHint() size.setHeight(self.fontHeight * 3 / 2) return size tortoisehg-2.10/tortoisehg/hgqt/fileview.py0000644000076400007640000013736512231647662020237 0ustar stevesteve# fileview.py - File diff, content, and annotation display widget # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import difflib import re from mercurial import util, patch from tortoisehg.util import hglib, colormap, thread2 from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qscilib, qtlib, blockmatcher, lexers from tortoisehg.hgqt import visdiff, filedata from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4 import Qsci qsci = qscilib.Scintilla DiffMode = 1 FileMode = 2 AnnMode = 3 class HgFileView(QFrame): "file diff, content, and annotation viewer" diffHeaderRegExp = re.compile("^@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@") linkActivated = pyqtSignal(QString) fileDisplayed = pyqtSignal(QString, QString) showMessage = pyqtSignal(QString) revisionSelected = pyqtSignal(int) shelveToolExited = pyqtSignal() newChunkList = pyqtSignal(QString, object) chunkSelectionChanged = pyqtSignal() grepRequested = pyqtSignal(unicode, dict) """Emitted (pattern, opts) when user request to search changelog""" def __init__(self, repoagent, parent): QFrame.__init__(self, parent) framelayout = QVBoxLayout(self) framelayout.setContentsMargins(0,0,0,0) l = QHBoxLayout() l.setContentsMargins(0,0,0,0) l.setSpacing(0) self._repoagent = repoagent repo = repoagent.rawRepo() # TODO: replace by repoagent if setRepo(bundlerepo) can be removed self.repo = repo self._diffs = [] self.changes = None self.changeselection = False self.chunkatline = {} self.excludemsg = _(' (excluded from the next commit)') self.topLayout = QVBoxLayout() self.labelhbox = hbox = QHBoxLayout() hbox.setContentsMargins(0,0,0,0) hbox.setSpacing(2) self.topLayout.addLayout(hbox) self.diffToolbar = QToolBar(_('Diff Toolbar')) self.diffToolbar.setIconSize(QSize(16,16)) self.diffToolbar.setStyleSheet(qtlib.tbstylesheet) hbox.addWidget(self.diffToolbar) self.filenamelabel = w = QLabel() w.setWordWrap(True) f = w.textInteractionFlags() w.setTextInteractionFlags(f | Qt.TextSelectableByMouse) w.linkActivated.connect(self.linkActivated) hbox.addWidget(w, 1) self.extralabel = w = QLabel() w.setWordWrap(True) w.linkActivated.connect(self.linkActivated) self.topLayout.addWidget(w) w.hide() framelayout.addLayout(self.topLayout) framelayout.addLayout(l, 1) hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(0) l.addLayout(hbox) self.blk = blockmatcher.BlockList(self) self.blksearch = blockmatcher.BlockList(self) self.sci = AnnotateView(repoagent, self) self._forceviewindicator = None hbox.addWidget(self.blk) hbox.addWidget(self.sci, 1) hbox.addWidget(self.blksearch) self.sci.showMessage.connect(self.showMessage) self.sci.setAnnotationEnabled(False) self.sci.setContextMenuPolicy(Qt.CustomContextMenu) self.sci.customContextMenuRequested.connect(self.menuRequest) self.annmarginclicked = False self.sci.marginClicked.connect(self.marginClicked) self.blk.linkScrollBar(self.sci.verticalScrollBar()) self.blk.setVisible(False) self.blksearch.linkScrollBar(self.sci.verticalScrollBar()) self.blksearch.setVisible(False) self.sci.setReadOnly(True) self.sci.setUtf8(True) keys = set((Qt.Key_Space,)) self.sci.installEventFilter(qscilib.KeyPressInterceptor(self, keys)) self.sci.setCaretLineVisible(False) # define markers for colorize zones of diff self.markerplus = self.sci.markerDefine(qsci.Background) self.markerminus = self.sci.markerDefine(qsci.Background) self.markertriangle = self.sci.markerDefine(qsci.Background) self.sci.setMarkerBackgroundColor(QColor('#B0FFA0'), self.markerplus) self.sci.setMarkerBackgroundColor(QColor('#A0A0FF'), self.markerminus) self.sci.setMarkerBackgroundColor(QColor('#FFA0A0'), self.markertriangle) self._checkedpix = qtlib.getcheckboxpixmap(QStyle.State_On, QColor('#B0FFA0'), self) self.inclmarker = self.sci.markerDefine(self._checkedpix, -1) self._uncheckedpix = qtlib.getcheckboxpixmap(QStyle.State_Off, QColor('#B0FFA0'), self) self.exclmarker = self.sci.markerDefine(self._uncheckedpix, -1) self.exclcolor = self.sci.markerDefine(qsci.Background, -1) self.sci.setMarkerBackgroundColor(QColor('lightgrey'), self.exclcolor) self.sci.setMarkerForegroundColor(QColor('darkgrey'), self.exclcolor) mask = (1 << self.inclmarker) | (1 << self.exclmarker) | \ (1 << self.exclcolor) self.sci.setMarginType(4, qsci.SymbolMargin) self.sci.setMarginMarkerMask(4, mask) self.markexcluded = QSettings().value('changes-mark-excluded').toBool() self.excludeindicator = -1 self.updateChunkIndicatorMarks() self.sci.setIndicatorDrawUnder(True, self.excludeindicator) self.sci.setIndicatorForegroundColor(QColor('gray'), self.excludeindicator) # hide margin 0 (markers) self.sci.setMarginType(0, qsci.SymbolMargin) self.sci.setMarginWidth(0, 0) self.searchbar = qscilib.SearchToolBar(hidable=True) self.searchbar.hide() self.searchbar.searchRequested.connect(self.find) self.searchbar.conditionChanged.connect(self.highlightText) self.layout().addWidget(self.searchbar) self._ctx = None self._filename = None self._status = None self._mode = None self._parent = 0 self._lostMode = None self._lastSearch = u'', False self.actionDiffMode = QAction(qtlib.geticon('view-diff'), _('View change as unified diff output'), self) self.actionDiffMode.setCheckable(True) self.actionDiffMode._mode = DiffMode self.actionFileMode = QAction(qtlib.geticon('view-file'), _('View change in context of file'), self) self.actionFileMode.setCheckable(True) self.actionFileMode._mode = FileMode self.actionAnnMode = QAction(qtlib.geticon('view-annotate'), _('annotate with revision numbers'), self) self.actionAnnMode.setCheckable(True) self.actionAnnMode._mode = AnnMode self.modeToggleGroup = QActionGroup(self) self.modeToggleGroup.addAction(self.actionDiffMode) self.modeToggleGroup.addAction(self.actionFileMode) self.modeToggleGroup.addAction(self.actionAnnMode) self.modeToggleGroup.triggered.connect(self._setModeByAction) # Next/Prev diff (in full file mode) self.actionNextDiff = QAction(qtlib.geticon('go-down'), _('Next diff (alt+down)'), self) self.actionNextDiff.setShortcut('Alt+Down') self.actionNextDiff.triggered.connect(self.nextDiff) self.actionPrevDiff = QAction(qtlib.geticon('go-up'), _('Previous diff (alt+up)'), self) self.actionPrevDiff.setShortcut('Alt+Up') self.actionPrevDiff.triggered.connect(self.prevDiff) self._setModeByAction(self.actionDiffMode) self.actionFirstParent = QAction('1', self) self.actionFirstParent.setCheckable(True) self.actionFirstParent.setChecked(True) self.actionFirstParent.setShortcut('CTRL+1') self.actionFirstParent.setToolTip(_('Show changes from first parent')) self.actionSecondParent = QAction('2', self) self.actionSecondParent.setCheckable(True) self.actionSecondParent.setShortcut('CTRL+2') self.actionSecondParent.setToolTip(_('Show changes from second parent')) self.parentToggleGroup = QActionGroup(self) self.parentToggleGroup.addAction(self.actionFirstParent) self.parentToggleGroup.addAction(self.actionSecondParent) self.parentToggleGroup.triggered.connect(self.setParent) self.actionFind = self.searchbar.toggleViewAction() self.actionFind.setIcon(qtlib.geticon('edit-find')) self.actionFind.setToolTip(_('Toggle display of text search bar')) self.actionFind.triggered.connect(self.searchbarTriggered) qtlib.newshortcutsforstdkey(QKeySequence.Find, self, self.showsearchbar) self.actionShelf = QAction('Shelve', self) self.actionShelf.setIcon(qtlib.geticon('shelve')) self.actionShelf.setToolTip(_('Open shelve tool')) self.actionShelf.triggered.connect(self.launchShelve) tb = self.diffToolbar tb.addAction(self.actionFirstParent) tb.addAction(self.actionSecondParent) tb.addSeparator() tb.addAction(self.actionDiffMode) tb.addAction(self.actionFileMode) tb.addAction(self.actionAnnMode) tb.addSeparator() tb.addAction(self.actionNextDiff) tb.addAction(self.actionPrevDiff) tb.addSeparator() tb.addAction(self.actionFind) tb.addAction(self.actionShelf) self.timer = QTimer(self) self.timer.setSingleShot(False) self.timer.timeout.connect(self.timerBuildDiffMarkers) def launchShelve(self): from tortoisehg.hgqt import shelve # TODO: pass self._filename dlg = shelve.ShelveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.shelveToolExited.emit() def setFont(self, font): self.sci.setFont(font) def loadSettings(self, qs, prefix): self.sci.loadSettings(qs, prefix) def saveSettings(self, qs, prefix): self.sci.saveSettings(qs, prefix) def setRepo(self, repo): self.repo = repo self.sci.repo = repo def updateChunkIndicatorMarks(self): ''' This method has some pre-requisites: - self.markexcluded and self.excludeindicator MUST be defined - self.excludeindicator MUST be set to -1 before calling this method for the first time ''' indicatortypes = (qsci.HiddenIndicator, qsci.StrikeIndicator) self.excludeindicator = self.sci.indicatorDefine( indicatortypes[self.markexcluded], self.excludeindicator) def enableChangeSelection(self, enable): 'Enable the use of a selection margin when a diff view is active' # Should only be called with True from the commit tool when it is in # a 'commit' mode and False for other uses self.changeselection = enable self._showChangeSelectMargin(enable) def updateChunk(self, chunk, exclude): 'change chunk exclusion state, update display when necessary' # returns True if the chunk state was changed if chunk.excluded == exclude: return False if exclude: chunk.excluded = True self.changes.excludecount += 1 self.sci.setReadOnly(False) llen = self.sci.text(chunk.lineno).length() self.sci.insertAt(self.excludemsg, chunk.lineno, llen-1) self.sci.setReadOnly(True) self.sci.markerDelete(chunk.lineno, self.inclmarker) self.sci.markerAdd(chunk.lineno, self.exclmarker) for i in xrange(chunk.linecount-1): self.sci.markerAdd(chunk.lineno+i+1, self.exclcolor) self.sci.fillIndicatorRange(chunk.lineno+1, 0, chunk.lineno+chunk.linecount, 0, self.excludeindicator) else: chunk.excluded = False self.changes.excludecount -= 1 self.sci.setReadOnly(False) llen = self.sci.text(chunk.lineno).length() mlen = len(self.excludemsg) pos = self.sci.positionFromLineIndex(chunk.lineno, llen-mlen-1) self.sci.SendScintilla(qsci.SCI_SETTARGETSTART, pos) self.sci.SendScintilla(qsci.SCI_SETTARGETEND, pos + mlen) self.sci.SendScintilla(qsci.SCI_REPLACETARGET, 0, '') self.sci.setReadOnly(True) self.sci.markerDelete(chunk.lineno, self.exclmarker) self.sci.markerAdd(chunk.lineno, self.inclmarker) for i in xrange(chunk.linecount-1): self.sci.markerDelete(chunk.lineno+i+1, self.exclcolor) self.sci.clearIndicatorRange(chunk.lineno+1, 0, chunk.lineno+chunk.linecount, 0, self.excludeindicator) return True @pyqtSlot(QAction) def _setModeByAction(self, action): 'One of the mode toolbar buttons has been toggled' mode = action._mode self._lostMode = mode if mode != self._mode: self._mode = mode self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) self.blk.setVisible(mode != DiffMode) self.sci.setAnnotationEnabled(mode == AnnMode) self.displayFile(self._filename, self._status) def setMode(self, mode): """Switch view to DiffMode/FileMode/AnnMode if available for the current content; otherwise it will be switched later""" actionmap = dict((a._mode, a) for a in self.modeToggleGroup.actions()) try: action = actionmap[mode] except KeyError: raise ValueError('invalid mode: %r' % mode) if action.isEnabled(): if not action.isChecked(): action.trigger() # implies _setModeByAction() else: self._lostMode = mode @pyqtSlot(QAction) def setParent(self, action): if action.text() == '1': parent = 0 else: parent = 1 if self._parent != parent: self._parent = parent self.displayFile(self._filename, self._status) def restrictModes(self, candiff, canfile, canann): 'Disable modes based on content constraints' self.actionDiffMode.setEnabled(candiff) self.actionFileMode.setEnabled(canfile) self.actionAnnMode.setEnabled(canann) # Switch mode if necessary mode = self._mode if not candiff and mode == DiffMode and canfile: mode = FileMode if not canfile and mode != DiffMode: mode = DiffMode if self._lostMode is None: self._lostMode = self._mode if self._mode != mode: self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) self.blk.setVisible(mode != DiffMode) self.sci.setAnnotationEnabled(mode == AnnMode) self._mode = mode if self._mode == DiffMode: self.actionDiffMode.setChecked(True) elif self._mode == FileMode: self.actionFileMode.setChecked(True) else: self.actionAnnMode.setChecked(True) def setContext(self, ctx, ctx2=None): self._ctx = ctx self._ctx2 = ctx2 self.sci.setTabWidth(ctx._repo.tabwidth) self.actionAnnMode.setVisible(ctx.rev() != None) self.actionShelf.setVisible(ctx.rev() == None) self.actionFirstParent.setVisible(len(ctx.parents()) == 2) self.actionSecondParent.setVisible(len(ctx.parents()) == 2) self.actionFirstParent.setEnabled(len(ctx.parents()) == 2) self.actionSecondParent.setEnabled(len(ctx.parents()) == 2) def setSource(self, path, rev, line): self.revisionSelected.emit(rev) self.setContext(self.repo[rev]) self.displayFile(path, None) self.showLine(line) def showLine(self, line): if line < self.sci.lines(): self.sci.setCursorPosition(line, 0) @pyqtSlot() def clearDisplay(self): self._filename = None self._diffs = [] self.restrictModes(False, False, False) self.sci.setMarginWidth(1, 0) self.clearMarkup() def clearMarkup(self): self.sci.clear() self.blk.clear() self.blksearch.clear() # Setting the label to ' ' rather than clear() keeps the label # from disappearing during refresh, and tool layouts bouncing self.filenamelabel.setText(' ') self.extralabel.hide() self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) self.maxWidth = 0 self.changes = None self.chunkatline = {} self._showChangeSelectMargin(False) self.sci.showHScrollBar(False) def _showChangeSelectMargin(self, show): 'toggle the display of the diff change selection margin' self.sci.setMarginWidth(4, show and 15 or 0) self.sci.setMarginSensitivity(4, show) #@pyqtSlot(int, int, Qt.KeyboardModifiers) def marginClicked(self, margin, line, state): 'margin clicked event' if margin == 2: if self.annmarginclicked or state == Qt.ControlModifier: fctx, line = self.sci._links[line] self.setSource(hglib.tounicode(fctx.path()), fctx.rev(), line) else: self.annmarginclicked = True def disableClick(): self.annmarginclicked = False QTimer.singleShot(QApplication.doubleClickInterval(), disableClick) # mimic the default "border selection" behavior, # which is disabled when you use setMarginSensitivity() if state == Qt.ShiftModifier: sellinetop, selchartop, sellinebottom, selcharbottom = self.sci.getSelection() if sellinetop <= line: sline = sellinetop eline = line + 1 else: sline = line eline = sellinebottom if selcharbottom != 0: eline += 1 else: sline = line eline = line + 1 self.sci.setSelection(sline, 0, eline, 0) return if line not in self.chunkatline: return chunk = self.chunkatline[line] if self.updateChunk(chunk, not chunk.excluded): self.chunkSelectionChanged.emit() def _setupForceViewIndicator(self): if not self._forceviewindicator: self._forceviewindicator = self.sci.indicatorDefine(self.sci.PlainIndicator) self.sci.setIndicatorDrawUnder(True, self._forceviewindicator) self.sci.setIndicatorForegroundColor( QColor('blue'), self._forceviewindicator) # delay until next event-loop in order to complete mouse release self.sci.SCN_INDICATORRELEASE.connect(self.forceDisplayFile, Qt.QueuedConnection) def forceDisplayFile(self): if self.changes is not None: return self.sci.setText(_('Please wait while the file is opened ...')) # Wait a little to ensure that the "wait message" is displayed QTimer.singleShot(10, lambda: self.displayFile(self._filename, self._status, force=True)) def displayFile(self, filename=None, status=None, force=False): if isinstance(filename, (unicode, QString)): filename = hglib.fromunicode(filename) status = hglib.fromunicode(status) if filename and self._filename == filename: # Get the last visible line to restore it after reloading the editor lastCursorPosition = self.sci.getCursorPosition() lastScrollPosition = self.sci.firstVisibleLine() else: lastCursorPosition = (0, 0) lastScrollPosition = 0 self._filename, self._status = filename, status self.clearMarkup() self._diffs = [] if filename is None: self.restrictModes(False, False, False) return if self._ctx2: ctx2 = self._ctx2 elif self._parent == 0 or len(self._ctx.parents()) == 1: ctx2 = self._ctx.p1() else: ctx2 = self._ctx.p2() fd = filedata.FileData(self._ctx, ctx2, filename, status, self.changeselection, force=force) if fd.elabel: self.extralabel.setText(fd.elabel) self.extralabel.show() else: self.extralabel.hide() self.filenamelabel.setText(fd.flabel) uf = hglib.tounicode(filename) if not fd.isValid(): self.sci.setText(fd.error) self.sci.setLexer(None) self.sci.setFont(qtlib.getfont('fontlog').font()) self.sci.setMarginWidth(1, 0) self.blk.setVisible(False) self.restrictModes(False, False, False) self.newChunkList.emit(uf, None) forcedisplaymsg = filedata.forcedisplaymsg linkstart = fd.error.find(forcedisplaymsg) if linkstart >= 0: # add the link to force to view the data anyway self._setupForceViewIndicator() self.sci.fillIndicatorRange( 0, linkstart, 0, linkstart+len(forcedisplaymsg), self._forceviewindicator) return candiff = bool(fd.diff) canfile = bool(fd.contents or fd.ucontents) canann = bool(fd.contents) and type(self._ctx.rev()) is int if not candiff or not canfile: self.restrictModes(candiff, canfile, canann) else: self.actionDiffMode.setEnabled(True) self.actionFileMode.setEnabled(True) self.actionAnnMode.setEnabled(True) if self._lostMode: self._mode = self._lostMode if self._lostMode == DiffMode: self.actionDiffMode.trigger() elif self._lostMode == FileMode: self.actionFileMode.trigger() elif self._lostMode == AnnMode: self.actionAnnMode.trigger() self._lostMode = None self.blk.setVisible(self._mode != DiffMode) self.sci.setAnnotationEnabled(self._mode == AnnMode) if self._mode == DiffMode: self.sci.setMarginWidth(1, 0) lexer = lexers.difflexer(self) self.sci.setLexer(lexer) if lexer is None: self.sci.setFont(qtlib.getfont('fontlog').font()) if fd.changes: self._showChangeSelectMargin(True) self.changes = fd.changes self.sci.setText(hglib.tounicode(fd.diff)) for chunk in self.changes.hunks: self.chunkatline[chunk.lineno] = chunk self.sci.markerAdd(chunk.lineno, self.inclmarker) elif fd.diff: # trim first three lines, for example: # diff -r f6bfc41af6d7 -r c1b18806486d tortoisehg/hgqt/mq.py # --- a/tortoisehg/hgqt/mq.py # +++ b/tortoisehg/hgqt/mq.py out = fd.diff.split('\n', 3) if len(out) == 4: self.sci.setText(hglib.tounicode(out[3])) else: # there was an error or rename without diffs self.sci.setText(hglib.tounicode(fd.diff)) self.newChunkList.emit(uf, fd.changes) elif fd.ucontents: # subrepo summary and perhaps other data self.sci.setText(fd.ucontents) self.sci.setLexer(None) self.sci.setFont(qtlib.getfont('fontlog').font()) self.sci.setMarginWidth(1, 0) self.blk.setVisible(False) self.newChunkList.emit(uf, None) return elif fd.contents: lexer = lexers.getlexer(self.repo.ui, filename, fd.contents, self) self.sci.setLexer(lexer) if lexer is None: self.sci.setFont(qtlib.getfont('fontlog').font()) self.sci.setText(hglib.tounicode(fd.contents)) self.blk.setVisible(True) self.sci._updatemarginwidth() if self._mode == AnnMode: self.sci._updateannotation(self._ctx, filename) self.newChunkList.emit(uf, None) else: self.newChunkList.emit(uf, None) return # Recover the last cursor/scroll position self.sci.setCursorPosition(*lastCursorPosition) # Make sure that lastScrollPosition never exceeds the amount of # lines on the editor lastScrollPosition = min(lastScrollPosition, self.sci.lines() - 1) self.sci.verticalScrollBar().setValue(lastScrollPosition) self.highlightText(*self._lastSearch) uc = hglib.tounicode(fd.contents) or '' self.fileDisplayed.emit(uf, uc) if self._mode != DiffMode: self.blk.setVisible(True) self.blk.syncPageStep() self.blksearch.syncPageStep() if fd.contents and fd.olddata: if self.timer.isActive(): self.timer.stop() self._fd = fd self.timer.start() self.actionNextDiff.setEnabled(bool(self._diffs)) self.actionPrevDiff.setEnabled(bool(self._diffs)) lexer = self.sci.lexer() if lexer: font = self.sci.lexer().font(0) else: font = self.sci.font() fm = QFontMetrics(font) self.maxWidth = fm.maxWidth() lines = unicode(self.sci.text()).splitlines() if lines: # assume that the longest line has the largest width; # fm.width() is too slow to apply to each line. try: longestline = max(lines, key=len) except TypeError: # Python<2.5 has no key support longestline = max((len(l), l) for l in lines)[1] self.maxWidth += fm.width(longestline) self.updateScrollBar() # # These four functions are used by Shift+Cursor actions in revdetails # def nextLine(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x+1, y) def prevLine(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x-1, y) def nextCol(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x, y+1) def prevCol(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x, y-1) @pyqtSlot(unicode, bool, bool, bool) def find(self, exp, icase=True, wrap=False, forward=True): self.sci.find(exp, icase, wrap, forward) @pyqtSlot(unicode, bool) def highlightText(self, match, icase=False): self._lastSearch = match, icase self.sci.highlightText(match, icase) blk = self.blksearch blk.clear() blk.setUpdatesEnabled(False) blk.clear() for l in self.sci.highlightLines: blk.addBlock('s', l, l + 1) blk.setVisible(bool(match)) blk.setUpdatesEnabled(True) def loadSelectionIntoSearchbar(self): text = self.sci.selectedText() if text: self.searchbar.setPattern(text) @pyqtSlot(bool) def searchbarTriggered(self, checked): if checked: self.loadSelectionIntoSearchbar() @pyqtSlot() def showsearchbar(self): self.loadSelectionIntoSearchbar() self.searchbar.show() def verticalScrollBar(self): return self.sci.verticalScrollBar() # # file mode diff markers # @pyqtSlot() def timerBuildDiffMarkers(self): 'show modified and added lines in the self.blk margin' # The way the diff markers are generated differs between the DiffMode # and the other modes # In the DiffMode case, the marker positions are found by looking for # lines matching a regular expression representing a diff header, while # in all other cases we use the difflib.SequenceMatcher, which returns # a set of opcodes that must be parsed # In any case, the markers are generated incrementally. This function is # run by a timer, which each time that is called processes a bunch of # lines (when in DiffMode) or of opcodes (in all other modes). # When there are no more lines or opcodes to consume the timer is # stopped. self.sci.setUpdatesEnabled(False) self.blk.setUpdatesEnabled(False) if self._mode == DiffMode: if self._fd: self._fd = None self._diffs = [] self._linestoprocess = unicode(self.sci.text()).splitlines() self._firstlinetoprocess = 0 self._opcodes = True # Process linesPerBlock lines at a time linesPerBlock = 100 # Look for lines matching the "diff header" for n, line in enumerate(self._linestoprocess[:linesPerBlock]): if self.diffHeaderRegExp.match(line): diffLine = self._firstlinetoprocess + n self._diffs.append([diffLine, diffLine]) self.sci.markerAdd(diffLine, self.markerplus) self._linestoprocess = self._linestoprocess[linesPerBlock:] self._firstlinetoprocess += linesPerBlock if not self._linestoprocess: self._opcodes = False self._firstlinetoprocess = 0 else: if self._fd: olddata = self._fd.olddata.splitlines() newdata = self._fd.contents.splitlines() diff = difflib.SequenceMatcher(None, olddata, newdata) self._opcodes = diff.get_opcodes() self._fd = None self._diffs = [] elif isinstance(self._opcodes, bool): # catch self._mode changes while this thread is active self._opcodes = [] for tag, alo, ahi, blo, bhi in self._opcodes[:30]: if tag == 'replace': self._diffs.append([blo, bhi]) self.blk.addBlock('x', blo, bhi) for i in range(blo, bhi): self.sci.markerAdd(i, self.markertriangle) elif tag == 'insert': self._diffs.append([blo, bhi]) self.blk.addBlock('+', blo, bhi) for i in range(blo, bhi): self.sci.markerAdd(i, self.markerplus) elif tag in ('equal', 'delete'): pass else: raise ValueError, 'unknown tag %r' % (tag,) self._opcodes = self._opcodes[30:] if not self._opcodes: self.actionNextDiff.setEnabled(bool(self._diffs)) self.actionPrevDiff.setEnabled(False) self.timer.stop() self.sci.setUpdatesEnabled(True) self.blk.setUpdatesEnabled(True) def nextDiff(self): if not self._diffs: self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) return else: row, column = self.sci.getCursorPosition() for i, (lo, hi) in enumerate(self._diffs): if lo > row: last = (i == (len(self._diffs)-1)) self.sci.setCursorPosition(lo, 0) self.sci.verticalScrollBar().setValue(lo) break else: last = True self.actionNextDiff.setEnabled(not last) self.actionPrevDiff.setEnabled(True) def prevDiff(self): if not self._diffs: self.actionNextDiff.setEnabled(False) self.actionPrevDiff.setEnabled(False) return else: row, column = self.sci.getCursorPosition() for i, (lo, hi) in enumerate(reversed(self._diffs)): if hi < row: first = (i == (len(self._diffs)-1)) self.sci.setCursorPosition(lo, 0) self.sci.verticalScrollBar().setValue(lo) break else: first = True self.actionNextDiff.setEnabled(True) self.actionPrevDiff.setEnabled(not first) def nDiffs(self): return len(self._diffs) def editSelected(self, path, rev, line): """Open editor to show the specified file""" path = hglib.fromunicode(path) base = visdiff.snapshot(self.repo, [path], self.repo[rev])[0] files = [os.path.join(base, path)] pattern = hglib.fromunicode(self._lastSearch[0]) qtlib.editfiles(self.repo, files, line, pattern, self) def vdiff(self, path, **opts): path = hglib.fromunicode(path) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [path], opts) if dlg: dlg.exec_() @pyqtSlot(QPoint) def menuRequest(self, point): menu = self._createContextMenu(point) menu.exec_(self.sci.viewport().mapToGlobal(point)) menu.setParent(None) def _createContextMenu(self, point): menu = self.sci.createEditorContextMenu() line = self.sci.lineNearPoint(point) selection = self.sci.selectedText() def sreq(**opts): return lambda: self.grepRequested.emit(selection, opts) def sann(): self.searchbar.search(selection) self.searchbar.show() if self._mode != AnnMode: if self.changeselection: def toggleMarkExcluded(): self.markexcluded = not self.markexcluded self.updateChunkIndicatorMarks() QSettings().setValue('changes-mark-excluded', self.markexcluded) actmarkexcluded = menu.addAction(_('Mark excluded changes')) actmarkexcluded.setCheckable(True) actmarkexcluded.setChecked(self.markexcluded) actmarkexcluded.triggered.connect(toggleMarkExcluded) if selection: menu.addSeparator() menu.addAction(_('&Search in Current File'), sann) menu.addAction(_('Search in All &History'), sreq(all=True)) return menu menu.addSeparator() annoptsmenu = menu.addMenu(_('Annotate Op&tions')) annoptsmenu.addActions(self.sci.annotateOptionActions()) if line < 0 or line >= len(self.sci._links): return menu menu.addSeparator() fctx, line = self.sci._links[line] if selection: def sreq(**opts): return lambda: self.grepRequested.emit(selection, opts) def sann(): self.searchbar.search(selection) self.searchbar.show() menu.addSeparator() annsearchmenu = menu.addMenu(_('Search Selected Text')) annsearchmenu.addAction(_('In Current &File'), sann) annsearchmenu.addAction(_('In &Current Revision'), sreq(rev='.')) annsearchmenu.addAction(_('In &Original Revision'), sreq(rev=fctx.rev())) annsearchmenu.addAction(_('In All &History'), sreq(all=True)) data = [hglib.tounicode(fctx.path()), fctx.rev(), line] def annorig(): self.setSource(*data) def editorig(): self.editSelected(*data) def difflocal(): self.vdiff(data[0], rev=['rev(%d)' % data[1]]) menu.addSeparator() origrev = fctx.rev() anngotomenu = menu.addMenu(_('Go to')) annviewmenu = menu.addMenu(_('View File at')) anndiffmenu = menu.addMenu(_('Diff File to')) anngotomenu.addAction(_('&Originating Revision'), annorig) annviewmenu.addAction(_('&Originating Revision'), editorig) anndiffmenu.addAction(_('&Local'), difflocal) for pfctx in fctx.parents(): pdata = [hglib.tounicode(pfctx.path()), pfctx.changectx().rev(), line] def annparent(data): self.setSource(*data) def editparent(data): self.editSelected(*data) def diffparent(pdata, cdata=data): self.vdiff(cdata[0], rev=['rev(%d)' % d[1] for d in (pdata, cdata)]) for name, func, smenu in [(_('&Parent Revision (%d)') % pdata[1], annparent, anngotomenu), (_('&Parent Revision (%d)') % pdata[1], editparent, annviewmenu), (_('&Parent Revision (%d)') % pdata[1], diffparent, anndiffmenu)]: def add(name, func): action = smenu.addAction(name) action.data = pdata action.run = lambda: func(action.data) action.triggered.connect(action.run) add(name, func) return menu def resizeEvent(self, event): super(HgFileView, self).resizeEvent(event) self.updateScrollBar() def keyPressEvent(self, event): if event.key() == Qt.Key_Space: if self.changeselection: x, y = self.sci.getCursorPosition() chunk = self.chunkContainsLine(x) if self.updateChunk(chunk, not chunk.excluded): self.chunkSelectionChanged.emit() return return super(HgFileView, self).keyPressEvent(event) def chunkContainsLine(self, line): chunks = self.chunkatline if line in chunks: return chunks[line] line = max(i for i in chunks.keys() if i < line) return chunks[line] def updateScrollBar(self): sbWidth = self.sci.verticalScrollBar().width() scrollWidth = self.maxWidth + sbWidth - self.sci.width() self.sci.showHScrollBar(scrollWidth > 0) self.sci.horizontalScrollBar().setRange(0, scrollWidth) class AnnotateView(qscilib.Scintilla): 'QScintilla widget capable of displaying annotations' showMessage = pyqtSignal(QString) def __init__(self, repoagent, parent=None): super(AnnotateView, self).__init__(parent) self.setReadOnly(True) self.setMarginLineNumbers(1, True) self.setMarginType(2, qsci.TextMarginRightJustified) self.setMouseTracking(False) self._repoagent = repoagent repo = repoagent.rawRepo() # TODO: replace by repoagent if sci.repo = bundlerepo can be removed self.repo = repo self._annotation_enabled = False self._links = [] # by line self._anncache = {} # by rev self._revmarkers = {} # by rev self._lastrev = None diffopts = patch.diffopts(repo.ui, section='annotate') self._thread = AnnotateThread(self, diffopts=diffopts) self._thread.finished.connect(self.fillModel) self._initAnnotateOptionActions() self._repoagent.configChanged.connect(self.configChanged) self.configChanged() self._loadAnnotateSettings() def _loadAnnotateSettings(self): s = QSettings() wb = "Annotate/" for a in self._annoptactions: a.setChecked(s.value(wb + a.data().toString()).toBool()) if not util.any(a.isChecked() for a in self._annoptactions): self._annoptactions[-1].setChecked(True) # 'rev' by default self._setupLineAnnotation() def _saveAnnotateSettings(self): s = QSettings() wb = "Annotate/" for a in self._annoptactions: s.setValue(wb + a.data().toString(), a.isChecked()) def _initAnnotateOptionActions(self): self._annoptactions = [] for name, field in [(_('Show &Author'), 'author'), (_('Show &Date'), 'date'), (_('Show &Revision'), 'rev')]: a = QAction(name, self, checkable=True) a.setData(field) a.triggered.connect(self._updateAnnotateOption) self._annoptactions.append(a) @pyqtSlot() def _updateAnnotateOption(self): # make sure at least one option is checked if not util.any(a.isChecked() for a in self._annoptactions): self.sender().setChecked(True) self._setupLineAnnotation() self.fillModel() self._saveAnnotateSettings() def annotateOptionActions(self): """List of QAction for annotate options""" return list(self._annoptactions) def _setupLineAnnotation(self): def getauthor(fctx): return hglib.tounicode(hglib.username(fctx.user())) def getdate(fctx): return util.shortdate(fctx.date()) def getrev(fctx): return fctx.rev() aformat = [str(a.data().toString()) for a in self._annoptactions if a.isChecked()] tiprev = self.repo['tip'].rev() revwidth = len(str(tiprev)) annfields = { 'rev': ('%%%dd' % revwidth, getrev), 'author': ('%s', getauthor), 'date': ('%s', getdate), } annformat = [] annfunc = [] for fieldname in aformat: fielddata = annfields.get(fieldname, ()) if fielddata: annformat.append(fielddata[0]) annfunc.append(fielddata[1]) annformat = ' : '.join(annformat) self._anncache.clear() def lineannotation(fctx): rev = fctx.rev() ann = self._anncache.get(rev, None) if ann is None: ann = annformat % tuple([f(fctx) for f in annfunc]) self._anncache[rev] = ann return ann self._lineannotation = lineannotation @pyqtSlot() def configChanged(self): self.setIndentationWidth(self.repo.tabwidth) self.setTabWidth(self.repo.tabwidth) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self._thread.abort() return return super(AnnotateView, self).keyPressEvent(event) def mouseMoveEvent(self, event): self._emitRevisionHintAtLine(self.lineNearPoint(event.pos())) super(AnnotateView, self).mouseMoveEvent(event) def _emitRevisionHintAtLine(self, line): if line < 0: return try: fctx = self._links[line][0] if fctx.rev() != self._lastrev: s = hglib.get_revision_desc(fctx, self.annfile) self.showMessage.emit(s) self._lastrev = fctx.rev() except IndexError: pass def _updateannotation(self, ctx, filename): if ctx.rev() is None: return wsub, filename, ctx = hglib.getDeepestSubrepoContainingFile(filename, ctx) if wsub is None: # The file was not found in the repo context or its subrepos # This may happen for files that have been removed return self.ctx = ctx self.annfile = filename self._thread.abort() self._thread.start(ctx[filename]) @pyqtSlot() def fillModel(self): self._thread.wait() if self._thread.data is None: return self._links = list(self._thread.data) self._anncache.clear() self._updaterevmargin() self._updatemarkers() self._updatemarginwidth() def clear(self): super(AnnotateView, self).clear() self.clearMarginText() self.markerDeleteAll() def setAnnotationEnabled(self, enabled): """Enable / disable annotation""" self._annotation_enabled = enabled self._updatemarginwidth() self.setMouseTracking(enabled) if not enabled: self.markerDeleteAll() def isAnnotationEnabled(self): """True if annotation enabled and available""" return self._annotation_enabled def _updaterevmargin(self): """Update the content of margin area showing revisions""" s = self._margin_style # Workaround to set style of the current sci widget. # QsciStyle sends style data only to the first sci widget. # See qscintilla2/Qt4/qscistyle.cpp self.SendScintilla(qsci.SCI_STYLESETBACK, s.style(), s.paper()) self.SendScintilla(qsci.SCI_STYLESETFONT, s.style(), s.font().family().toAscii().data()) self.SendScintilla(qsci.SCI_STYLESETSIZE, s.style(), s.font().pointSize()) for i, (fctx, _origline) in enumerate(self._links): self.setMarginText(i, self._lineannotation(fctx), s) def _updatemarkers(self): """Update markers which colorizes each line""" self._redefinemarkers() for i, (fctx, _origline) in enumerate(self._links): m = self._revmarkers.get(fctx.rev()) if m is not None: self.markerAdd(i, m) def _redefinemarkers(self): """Redefine line markers according to the current revs""" curdate = self.ctx.date()[0] # make sure to colorize at least 1 year mindate = curdate - 365 * 24 * 60 * 60 self._revmarkers.clear() filectxs = iter(fctx for fctx, _origline in self._links) palette = colormap.makeannotatepalette(filectxs, curdate, maxcolors=32, maxhues=8, maxsaturations=16, mindate=mindate) for i, (color, fctxs) in enumerate(palette.iteritems()): self.markerDefine(qsci.Background, i) self.setMarkerBackgroundColor(QColor(color), i) for fctx in fctxs: self._revmarkers[fctx.rev()] = i @util.propertycache def _margin_style(self): """Style for margin area""" s = Qsci.QsciStyle() s.setPaper(QApplication.palette().color(QPalette.Window)) s.setFont(self.font()) return s @pyqtSlot() def _updatemarginwidth(self): self.setMarginsFont(self.font()) def lentext(s): return 'M' * (len(str(s)) + 2) # 2 for margin self.setMarginWidth(1, lentext(self.lines())) showannmargin = bool(self.isAnnotationEnabled() and self._anncache) if showannmargin: # add 2 for margin maxwidth = 2 + max(len(s) for s in self._anncache.itervalues()) self.setMarginWidth(2, 'M' * maxwidth) else: self.setMarginWidth(2, 0) self.setMarginSensitivity(2, showannmargin) class AnnotateThread(QThread): 'Background thread for annotating a file at a revision' def __init__(self, parent=None, diffopts=None): super(AnnotateThread, self).__init__(parent) self._threadid = None self._diffopts = diffopts @pyqtSlot(object) def start(self, fctx): self._fctx = fctx super(AnnotateThread, self).start() self.data = None @pyqtSlot() def abort(self): threadid = self._threadid if threadid is None: return try: thread2._async_raise(threadid, KeyboardInterrupt) self.wait() except ValueError: pass def run(self): assert self.currentThread() != qApp.thread() self._threadid = self.currentThreadId() try: try: data = [] for (fctx, line), _text in \ self._fctx.annotate(True, True, self._diffopts): data.append((fctx, line)) self.data = data except KeyboardInterrupt: pass finally: self._threadid = None del self._fctx tortoisehg-2.10/tortoisehg/hgqt/thgrepo.py0000644000076400007640000010525312231647662020064 0ustar stevesteve# thgrepo.py - TortoiseHg additions to key Mercurial classes # # Copyright 2010 George Marrows # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. # # See mercurial/extensions.py, comments to wrapfunction, for this approach # to extending repositories and change contexts. import os import sys import shutil import tempfile import re import time from PyQt4.QtCore import * from mercurial import hg, util, error, bundlerepo, extensions, filemerge, node from mercurial import merge, subrepo from mercurial import ui as uimod from mercurial.util import propertycache from hgext import mq from tortoisehg.util import hglib, paths from tortoisehg.util.patchctx import patchctx from tortoisehg.hgqt import cmdcore _repocache = {} _kbfregex = re.compile(r'^\.kbf/') _lfregex = re.compile(r'^\.hglf/') if 'THGDEBUG' in os.environ: def dbgoutput(*args): sys.stdout.write(' '.join([str(a) for a in args])+'\n') else: def dbgoutput(*args): pass # thgrepo.repository() will be deprecated def repository(_ui=None, path='', bundle=None): '''Returns a subclassed Mercurial repository to which new THG-specific methods have been added. The repository object is obtained using mercurial.hg.repository()''' if bundle: if _ui is None: _ui = uimod.ui() repo = bundlerepo.bundlerepository(_ui, path, bundle) repo.__class__ = _extendrepo(repo) agent = RepoAgent(repo) return agent.rawRepo() if path not in _repocache: if _ui is None: _ui = uimod.ui() try: repo = hg.repository(_ui, path) # get unfiltered repo in version safe manner repo = getattr(repo, 'unfiltered', lambda: repo)() repo.__class__ = _extendrepo(repo) agent = RepoAgent(repo) _repocache[path] = agent.rawRepo() return agent.rawRepo() except EnvironmentError: raise error.RepoError('Cannot open repository at %s' % path) if not os.path.exists(os.path.join(path, '.hg/')): del _repocache[path] # this error must be in local encoding raise error.RepoError('%s is not a valid repository' % path) return _repocache[path] class _LockStillHeld(Exception): 'Raised to abort status check due to lock existence' class RepoWatcher(QObject): """Notify changes of repository by optionally monitoring filesystem""" configChanged = pyqtSignal() repositoryChanged = pyqtSignal() repositoryDestroyed = pyqtSignal() workingBranchChanged = pyqtSignal() def __init__(self, repo, parent=None): super(RepoWatcher, self).__init__(parent) self.repo = repo self._fswatcher = None self.recordState() self._uimtime = time.time() def startMonitoring(self): """Start filesystem monitoring to notify changes automatically""" if not self._fswatcher: self._fswatcher = QFileSystemWatcher(self) self._fswatcher.directoryChanged.connect(self._pollChanges) self._fswatcher.fileChanged.connect(self._pollChanges) self._fswatcher.addPath(hglib.tounicode(self.repo.path)) self._fswatcher.addPath(hglib.tounicode(self.repo.spath)) self.addMissingPaths() self._fswatcher.blockSignals(False) def stopMonitoring(self): """Stop filesystem monitoring by removing all watched paths""" if not self._fswatcher: return self._fswatcher.blockSignals(True) # ignore pending events dirs = self._fswatcher.directories() if dirs: self._fswatcher.removePaths(dirs) files = self._fswatcher.files() if files: self._fswatcher.removePaths(files) # QTBUG-32917: On Windows, removePaths() fails to remove ".hg" and # ".hg/store" from the list, but actually they are not watched. # Thus, they cannot be watched again by the same fswatcher instance. if self._fswatcher.directories() or self._fswatcher.files(): dbgoutput('failed to remove paths - destroying watcher') self._fswatcher.setParent(None) self._fswatcher = None def isMonitoring(self): """True if filesystem monitor is running""" if not self._fswatcher: return False return not self._fswatcher.signalsBlocked() @pyqtSlot() def _pollChanges(self): '''Catch writes or deletions of files, or writes to .hg/ folder, most importantly lock files''' self.pollStatus() # filesystem monitor may be stopped inside pollStatus() if self.isMonitoring(): self.addMissingPaths() def addMissingPaths(self): 'Add files to watcher that may have been added or replaced' existing = [f for f in self._getwatchedfiles() if os.path.exists(f)] files = [unicode(f) for f in self._fswatcher.files()] for f in existing: if hglib.tounicode(f) not in files: dbgoutput('add file to watcher:', f) self._fswatcher.addPath(hglib.tounicode(f)) for f in self.repo.uifiles(): if f and os.path.exists(f) and hglib.tounicode(f) not in files: dbgoutput('add ui file to watcher:', f) self._fswatcher.addPath(hglib.tounicode(f)) def pollStatus(self): if not os.path.exists(self.repo.path): dbgoutput('Repository destroyed', self.repo.root) self.repositoryDestroyed.emit() return if self.locked(): dbgoutput('locked, aborting') return try: if self._checkdirstate(): dbgoutput('dirstate changed, exiting') return self._checkrepotime() self._checkuimtime() except _LockStillHeld: dbgoutput('lock still held - ignoring for now') def locked(self): if os.path.lexists(self.repo.join('wlock')): return True if os.path.lexists(self.repo.sjoin('lock')): return True return False def recordState(self): try: self._parentnodes = self._getrawparents() self._repomtime = self._getrepomtime() self._dirstatemtime = os.path.getmtime(self.repo.join('dirstate')) self._branchmtime = os.path.getmtime(self.repo.join('branch')) self._rawbranch = self.repo.opener('branch').read() except EnvironmentError: self._dirstatemtime = None self._branchmtime = None self._rawbranch = None def _getrawparents(self): try: return self.repo.opener('dirstate').read(40) except EnvironmentError: return None def _getwatchedfiles(self): 'Repository files which may be modified without locking' watchedfiles = [] if hasattr(self.repo, 'mq'): watchedfiles.append(self.repo.mq.join('series')) watchedfiles.append(self.repo.mq.join('status')) watchedfiles.append(self.repo.mq.join('guards')) watchedfiles.append(self.repo.join('patches.queue')) watchedfiles.append(self.repo.join('patches.queues')) return watchedfiles def _getrepofiles(self): watchedfiles = [self.repo.sjoin('00changelog.i')] watchedfiles.append(self.repo.sjoin('phaseroots')) watchedfiles.append(self.repo.join('localtags')) # no need to watch 'bookmarks' because repo._bookmarks.write touches # 00changelog.i (see bookmarks.bmstore.write) watchedfiles.append(self.repo.join('bookmarks.current')) return watchedfiles + self._getwatchedfiles() def _getrepomtime(self): 'Return the last modification time for the repo' mtime = [] for f in self._getrepofiles(): try: mtime.append(os.path.getmtime(f)) except EnvironmentError: pass if mtime: return max(mtime) def _checkrepotime(self): 'Check for new changelog entries, or MQ status changes' if self._repomtime < self._getrepomtime(): dbgoutput('detected repository change') if self.locked(): raise _LockStillHeld self.recordState() self.repositoryChanged.emit() def _checkdirstate(self): 'Check for new dirstate mtime, then working parent changes' try: mtime = os.path.getmtime(self.repo.join('dirstate')) except EnvironmentError: return False if mtime <= self._dirstatemtime: return False changed = self._checkparentchanges() or self._checkbranch() self._dirstatemtime = mtime return changed def _checkparentchanges(self): nodes = self._getrawparents() if nodes != self._parentnodes: dbgoutput('dirstate change found') if self.locked(): raise _LockStillHeld self.recordState() self.repositoryChanged.emit() return True return False def _checkbranch(self): try: mtime = os.path.getmtime(self.repo.join('branch')) except EnvironmentError: return False if mtime <= self._branchmtime: return False changed = self._checkbranchcontent() self._branchmtime = mtime return changed def _checkbranchcontent(self): try: newbranch = self.repo.opener('branch').read() except EnvironmentError: return False if newbranch != self._rawbranch: dbgoutput('branch time change') if self.locked(): raise _LockStillHeld self._rawbranch = newbranch self.workingBranchChanged.emit() return True return False def _checkuimtime(self): 'Check for modified config files, or a new .hg/hgrc file' try: files = self.repo.uifiles() mtime = max(os.path.getmtime(f) for f in files if os.path.isfile(f)) if mtime > self._uimtime: dbgoutput('config change detected') self._uimtime = mtime self.configChanged.emit() except (EnvironmentError, ValueError): pass class RepoAgent(QObject): """Proxy access to repository and keep its states up-to-date""" configChanged = pyqtSignal() repositoryChanged = pyqtSignal() repositoryDestroyed = pyqtSignal() workingBranchChanged = pyqtSignal() busyChanged = pyqtSignal(bool) outputReceived = pyqtSignal(unicode, unicode) progressReceived = pyqtSignal(unicode, object, unicode, unicode, object) def __init__(self, repo): QObject.__init__(self) self._repo = repo # TODO: remove repo-to-agent references later; all widgets should own # RepoAgent instead of thgrepository. repo._pyqtobj = self self._watcher = watcher = RepoWatcher(repo, self) watcher.configChanged.connect(self._onConfigChanged) watcher.repositoryChanged.connect(self._onRepositoryChanged) watcher.repositoryDestroyed.connect(self._onRepositoryDestroyed) watcher.workingBranchChanged.connect(self._onWorkingBranchChanged) self._cmdagent = cmdagent = cmdcore.CmdAgent(self) cmdagent.setWorkingDirectory(self.rootPath()) cmdagent.outputReceived.connect(self.outputReceived) cmdagent.progressReceived.connect(self.progressReceived) cmdagent.busyChanged.connect(self._onBusyChanged) self._busystubsess = cmdcore.runningCmdSession() def startMonitoringIfEnabled(self): """Start filesystem monitoring on repository open by RepoManager or running command finished""" repo = self._repo monitorrepo = repo.ui.config('tortoisehg', 'monitorrepo', 'always') if monitorrepo == 'never': dbgoutput('watching of F/S events is disabled by configuration') elif isinstance(repo, bundlerepo.bundlerepository): dbgoutput('not watching F/S events for bundle repository') elif monitorrepo == 'localonly' and paths.netdrive_status(repo.path): dbgoutput('not watching F/S events for network drive') elif self.isBusy(): dbgoutput('not watching F/S events while busy') else: self._watcher.startMonitoring() def stopMonitoring(self): """Stop filesystem monitoring on repository closed by RepoManager or command about to run""" self._watcher.stopMonitoring() def rawRepo(self): return self._repo def rootPath(self): return hglib.tounicode(self._repo.root) def pollStatus(self): """Force checking changes to emit corresponding signals""" if self._cmdagent.isBusy(): return # delayed until _onBusyChanged(False) self._watcher.pollStatus() @pyqtSlot() def _onConfigChanged(self): self._repo.invalidateui() self.configChanged.emit() @pyqtSlot() def _onRepositoryChanged(self): self._repo.thginvalidate() self.repositoryChanged.emit() @pyqtSlot() def _onRepositoryDestroyed(self): if self._repo.root in _repocache: del _repocache[self._repo.root] self.stopMonitoring() # avoid further changed/destroyed signals self.repositoryDestroyed.emit() @pyqtSlot() def _onWorkingBranchChanged(self): self._repo.thginvalidate() self.workingBranchChanged.emit() def isBusy(self): return self._cmdagent.isBusy() # TODO: remove _increment/decrementBusyCount def _incrementBusyCount(self): self._cmdagent._enqueueSession(self._busystubsess) def _decrementBusyCount(self): self._cmdagent._dequeueSession(self._busystubsess) if self._cmdagent.isBusy(): # TODO: maybe this is necessary on each commandFinished # A lot of logic will depend on invalidation happening within # the context of this call. Signals will not be emitted till later, # but we at least invalidate cached data in the repository self._repo.thginvalidate() @pyqtSlot(bool) def _onBusyChanged(self, busy): if busy: self.stopMonitoring() else: self._watcher.pollStatus() self.startMonitoringIfEnabled() self.busyChanged.emit(busy) def runCommand(self, cmdline, parent=None, display=None, worker=None): """Executes a single command asynchronously in this repository""" return self._cmdagent.runCommand(cmdline, parent, display, worker) def runCommandSequence(self, cmdlines, parent=None, display=None, worker=None): """Executes a series of commands asynchronously in this repository""" return self._cmdagent.runCommandSequence(cmdlines, parent, display, worker) def abortCommands(self): """Abort running and queued commands""" self._cmdagent.abortCommands() def _normreporoot(path): """Normalize repo root path in the same manner as localrepository""" # see localrepo.localrepository and scmutil.vfs lpath = hglib.fromunicode(path) lpath = os.path.realpath(util.expandpath(lpath)) return hglib.tounicode(lpath) class RepoManager(QObject): """Cache open RepoAgent instances and bundle their signals""" repositoryOpened = pyqtSignal(unicode) repositoryClosed = pyqtSignal(unicode) configChanged = pyqtSignal(unicode) repositoryChanged = pyqtSignal(unicode) repositoryDestroyed = pyqtSignal(unicode) _SIGNALMAP = [ # source, dest (SIGNAL('configChanged()'), SIGNAL('configChanged(QString)')), (SIGNAL('repositoryChanged()'), SIGNAL('repositoryChanged(QString)')), (SIGNAL('repositoryDestroyed()'), SIGNAL('repositoryDestroyed(QString)')), ] def __init__(self, ui, parent=None): super(RepoManager, self).__init__(parent) self._ui = ui self._openagents = {} # path: (agent, refcount) self._sigmappers = [] for _sig, slot in self._SIGNALMAP: mapper = QSignalMapper(self) self._sigmappers.append(mapper) QObject.connect(mapper, SIGNAL('mapped(QString)'), self, slot) def openRepoAgent(self, path): """Return RepoAgent for the specified path and increment refcount""" path = _normreporoot(path) if path in self._openagents: agent, refcount = self._openagents[path] self._openagents[path] = (agent, refcount + 1) return agent # TODO: move repository creation from thgrepo.repository() self._ui.debug('opening repo: %s\n' % hglib.fromunicode(path)) agent = repository(self._ui, hglib.fromunicode(path))._pyqtobj assert agent.parent() is None agent.setParent(self) for (sig, _slot), mapper in zip(self._SIGNALMAP, self._sigmappers): QObject.connect(agent, sig, mapper, SLOT('map()')) mapper.setMapping(agent, agent.rootPath()) agent.startMonitoringIfEnabled() assert agent.rootPath() == path self._openagents[path] = (agent, 1) self.repositoryOpened.emit(path) return agent def releaseRepoAgent(self, path): """Decrement refcount of RepoAgent and close it if possible""" path = _normreporoot(path) agent, refcount = self._openagents[path] if refcount > 1: self._openagents[path] = (agent, refcount - 1) return self._ui.debug('closing repo: %s\n' % hglib.fromunicode(path)) agent, _refcount = self._openagents.pop(path) agent.stopMonitoring() # TODO: disconnected automatically if _repocache does not exist for (sig, _slot), mapper in zip(self._SIGNALMAP, self._sigmappers): QObject.disconnect(agent, sig, mapper, SLOT('map()')) mapper.removeMappings(agent) agent.setParent(None) self.repositoryClosed.emit(path) def repoAgent(self, path): """Peek open RepoAgent for the specified path without refcount change; None for unknown path""" path = _normreporoot(path) return self._openagents.get(path, (None, 0))[0] def repoRootPaths(self): """Return list of root paths of open repositories""" return self._openagents.keys() _uiprops = '''_uifiles postpull tabwidth maxdiff deadbranches _exts _thghiddentags displayname summarylen shortname mergetools namedbranches'''.split() _thgrepoprops = '''_thgmqpatchnames thgmqunappliedpatches _branchheads'''.split() def _extendrepo(repo): class thgrepository(repo.__class__): def __getitem__(self, changeid): '''Extends Mercurial's standard __getitem__() method to a) return a thgchangectx with additional methods b) return a patchctx if changeid is the name of an MQ unapplied patch c) return a patchctx if changeid is an absolute patch path ''' # Mercurial's standard changectx() (rather, lookup()) # implies that tags and branch names live in the same namespace. # This code throws patch names in the same namespace, but as # applied patches have a tag that matches their patch name this # seems safe. if changeid in self.thgmqunappliedpatches: q = self.mq # must have mq to pass the previous if return genPatchContext(self, q.join(changeid), rev=changeid) elif type(changeid) is str and '\0' not in changeid and \ os.path.isabs(changeid) and os.path.isfile(changeid): return genPatchContext(repo, changeid) changectx = super(thgrepository, self).__getitem__(changeid) changectx.__class__ = _extendchangectx(changectx) return changectx def hgchangectx(self, changeid): '''Returns unwrapped changectx or workingctx object''' # This provides temporary workaround for troubles caused by class # extension: e.g. changectx(n) != thgchangectx(n). # thgrepository and thgchangectx should be removed in some way. return super(thgrepository, self).__getitem__(changeid) @propertycache def _thghiddentags(self): ht = self.ui.config('tortoisehg', 'hidetags', '') return [t.strip() for t in ht.split()] @propertycache def thgmqunappliedpatches(self): '''Returns a list of (patch name, patch path) of all self's unapplied MQ patches, in patch series order, first unapplied patch first.''' if not hasattr(self, 'mq'): return [] q = self.mq applied = set([p.name for p in q.applied]) return [pname for pname in q.series if not pname in applied] @propertycache def _thgmqpatchnames(self): '''Returns all tag names used by MQ patches. Returns [] if MQ not in use.''' return hglib.getmqpatchtags(self) @property def thgactivemqname(self): '''Currenty-active qqueue name (see hgext/mq.py:qqueue)''' return hglib.getcurrentqqueue(self) @propertycache def _uifiles(self): cfg = self.ui._ucfg files = set() for line in cfg._source.values(): f = line.rsplit(':', 1)[0] files.add(f) files.add(self.join('hgrc')) return files @propertycache def _exts(self): lclexts = [] allexts = [n for n,m in extensions.extensions()] for name, path in self.ui.configitems('extensions'): if name.startswith('hgext.'): name = name[6:] if name in allexts: lclexts.append(name) return lclexts @propertycache def postpull(self): pp = self.ui.config('tortoisehg', 'postpull') if pp in ('rebase', 'update', 'fetch', 'updateorrebase'): return pp return 'none' @propertycache def tabwidth(self): tw = self.ui.config('tortoisehg', 'tabwidth') try: tw = int(tw) tw = min(tw, 16) return max(tw, 2) except (ValueError, TypeError): return 8 @propertycache def maxdiff(self): maxdiff = self.ui.config('tortoisehg', 'maxdiff') try: maxdiff = int(maxdiff) if maxdiff < 1: return sys.maxint except (ValueError, TypeError): maxdiff = 1024 # 1MB by default return maxdiff * 1024 @propertycache def summarylen(self): slen = self.ui.config('tortoisehg', 'summarylen') try: slen = int(slen) if slen < 10: return 80 except (ValueError, TypeError): slen = 80 return slen @propertycache def deadbranches(self): db = self.ui.config('tortoisehg', 'deadbranch', '') return [b.strip() for b in db.split(',')] @propertycache def displayname(self): 'Display name is for window titles and similar' if self.ui.configbool('tortoisehg', 'fullpath'): name = self.root elif self.ui.config('web', 'name', False): name = self.ui.config('web', 'name') else: name = os.path.basename(self.root) return hglib.tounicode(name) @propertycache def shortname(self): 'Short name is for tables, tabs, and sentences' if self.ui.config('web', 'name', False): name = self.ui.config('web', 'name') else: name = os.path.basename(self.root) return hglib.tounicode(name) @propertycache def mergetools(self): seen, installed = [], [] for key, value in self.ui.configitems('merge-tools'): t = key.split('.')[0] if t not in seen: seen.append(t) if filemerge._findtool(self.ui, t): installed.append(t) return installed @propertycache def namedbranches(self): allbranches = self.branchtags() openbrnodes = [] for br in allbranches.iterkeys(): openbrnodes.extend(self.branchheads(br, closed=False)) dead = self.deadbranches return sorted(br for br, n in allbranches.iteritems() if n in openbrnodes and br not in dead) @propertycache def _branchheads(self): heads = [] for branchname, nodes in self.branchmap().iteritems(): heads.extend(nodes) return heads def uifiles(self): 'Returns complete list of config files' return self._uifiles def extensions(self): 'Returns list of extensions enabled in this repository' return self._exts def thgmqtag(self, tag): 'Returns true if `tag` marks an applied MQ patch' return tag in self._thgmqpatchnames def thgshelves(self): self.shelfdir = sdir = self.join('shelves') if os.path.isdir(sdir): def getModificationTime(x): try: return os.path.getmtime(os.path.join(sdir, x)) except EnvironmentError: return 0 shelves = sorted(os.listdir(sdir), key=getModificationTime, reverse=True) return [s for s in shelves if \ os.path.isfile(os.path.join(self.shelfdir, s))] return [] def makeshelf(self, patch): if not os.path.exists(self.shelfdir): os.mkdir(self.shelfdir) f = open(os.path.join(self.shelfdir, patch), "wb") f.close() def thginvalidate(self): 'Should be called when mtime of repo store/dirstate are changed' self.dirstate.invalidate() if not isinstance(repo, bundlerepo.bundlerepository): self.invalidate() # mq.queue.invalidate does not handle queue changes, so force # the queue object to be rebuilt if 'mq' in self.__dict__: delattr(self, 'mq') for a in _thgrepoprops + _uiprops: if a in self.__dict__: delattr(self, a) def invalidateui(self): 'Should be called when mtime of ui files are changed' self.ui = uimod.ui() self.ui.readconfig(self.join('hgrc')) for a in _uiprops: if a in self.__dict__: delattr(self, a) # TODO: replace manual busycount handling by RepoAgent's def incrementBusyCount(self): 'A GUI widget is starting a transaction' self._pyqtobj._incrementBusyCount() def decrementBusyCount(self): 'A GUI widget has finished a transaction' self._pyqtobj._decrementBusyCount() def thgbackup(self, path): 'Make a backup of the given file in the repository "trashcan"' # The backup name will be the same as the orginal file plus '.bak' trashcan = self.join('Trashcan') if not os.path.isdir(trashcan): os.mkdir(trashcan) if not os.path.exists(path): return name = os.path.basename(path) root, ext = os.path.splitext(name) dest = tempfile.mktemp(ext+'.bak', root+'_', trashcan) shutil.copyfile(path, dest) def isStandin(self, path): if 'largefiles' in self.extensions(): if _lfregex.match(path): return True if 'largefiles' in self.extensions() or 'kbfiles' in self.extensions(): if _kbfregex.match(path): return True return False def removeStandin(self, path): if 'largefiles' in self.extensions(): path = _lfregex.sub('', path) if 'largefiles' in self.extensions() or 'kbfiles' in self.extensions(): path = _kbfregex.sub('', path) return path def bfStandin(self, path): return '.kbf/' + path def lfStandin(self, path): return '.hglf/' + path return thgrepository _maxchangectxclscache = 10 _changectxclscache = {} # parentcls: extendedcls def _extendchangectx(changectx): # cache extended changectx class, since we may create bunch of instances parentcls = changectx.__class__ try: return _changectxclscache[parentcls] except KeyError: pass # in case each changectx instance is wrapped by some extension, there's # limit on cache size. it may be possible to use weakref.WeakKeyDictionary # on Python 2.5 or later. if len(_changectxclscache) >= _maxchangectxclscache: _changectxclscache.clear() _changectxclscache[parentcls] = cls = _createchangectxcls(parentcls) return cls def _createchangectxcls(parentcls): class thgchangectx(parentcls): def sub(self, path): srepo = super(thgchangectx, self).sub(path) if isinstance(srepo, subrepo.hgsubrepo): r = srepo._repo # get unfiltered repo in version safe manner r = getattr(r, 'unfiltered', lambda: r)() r.__class__ = _extendrepo(r) srepo._repo = r return srepo def thgtags(self): '''Returns all unhidden tags for self''' htlist = self._repo._thghiddentags return [tag for tag in self.tags() if tag not in htlist] def thgwdparent(self): '''True if self is a parent of the working directory''' return self.rev() in [ctx.rev() for ctx in self._repo.parents()] def _thgmqpatchtags(self): '''Returns the set of self's tags which are MQ patch names''' mytags = set(self.tags()) patchtags = self._repo._thgmqpatchnames result = mytags.intersection(patchtags) assert len(result) <= 1, "thgmqpatchname: rev has more than one tag in series" return result def thgmqappliedpatch(self): '''True if self is an MQ applied patch''' return self.rev() is not None and bool(self._thgmqpatchtags()) def thgmqunappliedpatch(self): return False def thgid(self): return self._node def thgmqpatchname(self): '''Return self's MQ patch name. AssertionError if self not an MQ patch''' patchtags = self._thgmqpatchtags() assert len(patchtags) == 1, "thgmqpatchname: called on non-mq patch" return list(patchtags)[0] def thgbranchhead(self): '''True if self is a branch head''' return self.node() in self._repo._branchheads def thgmqoriginalparent(self): '''The revisionid of the original patch parent''' if not self.thgmqunappliedpatch() and not self.thgmqappliedpatch(): return '' try: patchpath = self._repo.mq.join(self.thgmqpatchname()) mqoriginalparent = mq.patchheader(patchpath).parent except EnvironmentError: return '' return mqoriginalparent def changesToParent(self, whichparent): parent = self.parents()[whichparent] return self._repo.status(parent.node(), self.node())[:3] def longsummary(self): if self._repo.ui.configbool('tortoisehg', 'longsummary'): limit = 80 else: limit = None return hglib.longsummary(self.description(), limit) def hasStandin(self, file): if 'largefiles' in self._repo.extensions(): if self._repo.lfStandin(file) in self.manifest(): return True elif 'largefiles' in self._repo.extensions() or 'kbfiles' in self._repo.extensions(): if self._repo.bfStandin(file) in self.manifest(): return True return False def isStandin(self, path): return self._repo.isStandin(path) def removeStandin(self, path): return self._repo.removeStandin(path) def findStandin(self, file): if 'largefiles' in self._repo.extensions(): if self._repo.lfStandin(file) in self.manifest(): return self._repo.lfStandin(file) return self._repo.bfStandin(file) return thgchangectx _pctxcache = {} def genPatchContext(repo, patchpath, rev=None): global _pctxcache try: if os.path.exists(patchpath) and patchpath in _pctxcache: cachedctx = _pctxcache[patchpath] if cachedctx._mtime == os.path.getmtime(patchpath) and \ cachedctx._fsize == os.path.getsize(patchpath): return cachedctx except EnvironmentError: pass # create a new context object ctx = patchctx(patchpath, repo, rev=rev) _pctxcache[patchpath] = ctx return ctx def recursiveMergeStatus(repo): ms = merge.mergestate(repo) for wfile in ms: yield repo.root, wfile, ms[wfile] try: wctx = repo[None] for s in wctx.substate: sub = wctx.sub(s) if isinstance(sub, subrepo.hgsubrepo): for root, file, status in recursiveMergeStatus(sub._repo): yield root, file, status except (EnvironmentError, error.Abort, error.RepoError): pass def relatedRepositories(repoid): 'Yields root paths for local related repositories' from tortoisehg.hgqt import reporegistry, repotreemodel if repoid == node.nullid: # empty repositories shouldn't be related return f = QFile(reporegistry.settingsfilename()) f.open(QIODevice.ReadOnly) try: for e in repotreemodel.iterRepoItemFromXml(f): if e.basenode() == repoid: # TODO: both in unicode because this is Qt-layer function? yield e.rootpath(), hglib.fromunicode(e.shortname()) except: f.close() raise else: f.close() def isBfStandin(path): return _kbfregex.match(path) def isLfStandin(path): return _lfregex.match(path) tortoisehg-2.10/tortoisehg/hgqt/manifestmodel.py0000644000076400007640000003705112170335562021236 0ustar stevesteve# manifestmodel.py - Model for TortoiseHg manifest view # # Copyright (C) 2009-2010 LOGILAB S.A. # Copyright (C) 2010 Yuya Nishihara # # 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. import os, itertools, fnmatch from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util from mercurial.subrepo import hgsubrepo from tortoisehg.util import hglib from tortoisehg.hgqt import qtlib, status, visdiff class ManifestModel(QAbstractItemModel): """ Qt model to display a hg manifest, ie. the tree of files at a given revision. To be used with a QTreeView. """ StatusRole = Qt.UserRole + 1 """Role for file change status""" _fileiconprovider = QFileIconProvider() _icons = {} def __init__(self, repo, rev=None, namefilter=None, statusfilter='MASC', parent=None): QAbstractItemModel.__init__(self, parent) self._diricon = QApplication.style().standardIcon(QStyle.SP_DirIcon) self._fileicon = QApplication.style().standardIcon(QStyle.SP_FileIcon) self._repo = repo self._rev = rev self._subinfo = {} self._namefilter = namefilter assert util.all(c in 'MARSC' for c in statusfilter) self._statusfilter = statusfilter def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return if role == Qt.DecorationRole: return self.fileIcon(index) if role == self.StatusRole: return self.fileStatus(index) e = index.internalPointer() if role in (Qt.DisplayRole, Qt.EditRole): return e.name def filePath(self, index): """Return path at the given index [unicode]""" if not index.isValid(): return '' return index.internalPointer().path def fileSubrepoCtx(self, index): """Return the subrepo context of the specified index""" path = self.filePath(index) return self.fileSubrepoCtxFromPath(path) def fileSubrepoCtxFromPath(self, path): """Return the subrepo context of the specified file""" if not path: return None, path for subpath in sorted(self._subinfo.keys())[::-1]: if path.startswith(subpath + '/'): return self._subinfo[subpath]['ctx'], path[len(subpath)+1:] return None, path def subrepoType(self, index): """Return the subrepo type the specified index""" path = self.filePath(index) return self.subrepoTypeFromPath(path) def subrepoTypeFromPath(self, path): """Return the subrepo type of the specified subrepo""" if not path: return None try: substate = self._subinfo[path] return substate['substate'][2] except: return None def fileIcon(self, index): if not index.isValid(): if self.isDir(index): return self._diricon else: return self._fileicon e = index.internalPointer() ic = e.icon if not ic: if self.isDir(index): ic = self._diricon else: ext = os.path.splitext(e.path)[1] if not ext: ic = self._fileicon else: ic = self._icons.get(ext, None) if not ic: ic = self._fileiconprovider.icon( QFileInfo(self._wjoin(e.path))) if not ic.availableSizes(): ic = self._fileicon self._icons[ext] = ic e.seticon(ic) if not e.status: return ic st = status.statusTypes[e.status] if st.icon: icOverlay = qtlib.geticon(st.icon[:-4]) if e.status == 'S': _subrepoType2IcoMap = { 'hg': 'hg', 'git': 'thg-git-subrepo', 'svn': 'thg-svn-subrepo', } stype = self.subrepoType(index) if stype in _subrepoType2IcoMap: ic = qtlib.geticon(_subrepoType2IcoMap[stype]) ic = qtlib.getoverlaidicon(ic, icOverlay) return ic def fileStatus(self, index): """Return the change status of the specified file""" if not index.isValid(): return e = index.internalPointer() return e.status def isDir(self, index): if not index.isValid(): return True # root entry must be a directory e = index.internalPointer() if e.status == 'S': # Consider subrepos as dirs as well return True else: return len(e) != 0 def mimeData(self, indexes): def preparefiles(): files = [self.filePath(i) for i in indexes if i.isValid()] if self._rev is not None: base, _fns = visdiff.snapshot(self._repo, files, self._repo[self._rev]) else: # working copy base = self._repo.root return iter(os.path.join(base, e) for e in files) m = QMimeData() m.setUrls([QUrl.fromLocalFile(e) for e in preparefiles()]) return m def mimeTypes(self): return ['text/uri-list'] def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled f = Qt.ItemIsEnabled | Qt.ItemIsSelectable if not (self.isDir(index) or self.fileStatus(index) == 'R'): f |= Qt.ItemIsDragEnabled return f def index(self, row, column, parent=QModelIndex()): try: return self.createIndex(row, column, self._parententry(parent).at(row)) except IndexError: return QModelIndex() def indexFromPath(self, path, column=0): """Return index for the specified path if found [unicode] If not found, returns invalid index. """ if not path: return QModelIndex() e = self._rootentry paths = path and unicode(path).split('/') or [] try: for p in paths: e = e[p] except KeyError: return QModelIndex() return self.createIndex(e.parent.index(e.name), column, e) def parent(self, index): if not index.isValid(): return QModelIndex() e = index.internalPointer() if e.path: return self.indexFromPath(e.parent.path, index.column()) else: return QModelIndex() def _parententry(self, parent): if parent.isValid(): return parent.internalPointer() else: return self._rootentry def rowCount(self, parent=QModelIndex()): return len(self._parententry(parent)) def columnCount(self, parent=QModelIndex()): return 1 @pyqtSlot(unicode) def setNameFilter(self, pattern): """Filter file name by partial match of glob pattern""" pattern = pattern and unicode(pattern) or None if self._namefilter == pattern: return self._namefilter = pattern self._rebuildrootentry() @property def nameFilter(self): """Return the current name filter if available; otherwise None""" return self._namefilter @pyqtSlot(str) def setStatusFilter(self, status): """Filter file tree by change status 'MARSC'""" status = str(status) assert util.all(c in 'MARSC' for c in status) if self._statusfilter == status: return # for performance reason self._statusfilter = status self._rebuildrootentry() @property def statusFilter(self): """Return the current status filter""" return self._statusfilter def _wjoin(self, path): return os.path.join(hglib.tounicode(self._repo.root), unicode(path)) @property def _rootentry(self): try: return self.__rootentry except (AttributeError, TypeError): self.__rootentry = self._newrootentry() return self.__rootentry def _rebuildrootentry(self): """Rebuild the tree of files and directories""" roote = self._newrootentry() self.layoutAboutToBeChanged.emit() try: oldindexmap = [(i, self.filePath(i)) for i in self.persistentIndexList()] self.__rootentry = roote for oi, path in oldindexmap: self.changePersistentIndex(oi, self.indexFromPath(path)) finally: self.layoutChanged.emit() def _newrootentry(self): """Create the tree of files and directories and return its root""" def pathinstatus(path, status, uncleanpaths): """Test path is included by the status filter""" if util.any(c in self._statusfilter and path in e for c, e in status.iteritems()): return True if 'C' in self._statusfilter and path not in uncleanpaths: return True return False def getctxtreeinfo(ctx): """ Get the context information that is relevant to populating the tree """ status = dict(zip(('M', 'A', 'R'), (set(a) for a in self._repo.status(ctx.parents()[0], ctx)[:3]))) uncleanpaths = status['M'] | status['A'] | status['R'] files = itertools.chain(ctx.manifest(), status['R']) return status, uncleanpaths, files def addfilestotree(treeroot, files, status, uncleanpaths): """Add files to the tree according to their state""" if self._namefilter: files = fnmatch.filter(files, '*%s*' % self._namefilter) for path in files: if not pathinstatus(path, status, uncleanpaths): continue origpath = path path = self._repo.removeStandin(path) e = treeroot for p in hglib.tounicode(path).split('/'): if not p in e: e.addchild(p) e = e[p] for st, filesofst in status.iteritems(): if origpath in filesofst: e.setstatus(st) break else: e.setstatus('C') # Add subrepos to the tree def addrepocontentstotree(roote, ctx, toproot=''): subpaths = ctx.substate.keys() for path in subpaths: if not 'S' in self._statusfilter: break e = roote pathelements = hglib.tounicode(path).split('/') for p in pathelements[:-1]: if not p in e: e.addchild(p) e = e[p] p = pathelements[-1] if not p in e: e.addchild(p) e = e[p] e.setstatus('S') # If the subrepo exists in the working directory # and it is a mercurial subrepo, # add the files that it contains to the tree as well, according # to the status filter abspath = os.path.join(ctx._repo.root, path) if os.path.isdir(abspath): # Add subrepo files to the tree substate = ctx.substate[path] # Add the subrepo info to the _subinfo dictionary: # The value is the subrepo context, while the key is # the path of the subrepo relative to the topmost repo if toproot: # Note that we cannot use os.path.join() because we # need path items to be separated by "/" toprelpath = '/'.join([toproot, path]) else: toprelpath = path toprelpath = util.pconvert(toprelpath) self._subinfo[toprelpath] = \ {'substate': substate, 'ctx': None} srev = substate[1] sub = ctx.sub(path) if srev and isinstance(sub, hgsubrepo): srepo = sub._repo if srev in srepo: sctx = srepo[srev] self._subinfo[toprelpath]['ctx'] = sctx # Add the subrepo contents to the tree e = addrepocontentstotree(e, sctx, toprelpath) # Add regular files to the tree status, uncleanpaths, files = getctxtreeinfo(ctx) addfilestotree(roote, files, status, uncleanpaths) return roote # Clear the _subinfo self._subinfo = {} roote = _Entry() ctx = self._repo[self._rev] addrepocontentstotree(roote, ctx) roote.sort() return roote class _Entry(object): """Each file or directory""" def __init__(self, name='', parent=None): self._name = name self._parent = parent self._status = None self._icon = None self._child = {} self._nameindex = [] @property def parent(self): return self._parent @property def path(self): if self.parent is None or not self.parent.name: return self.name else: return self.parent.path + '/' + self.name @property def name(self): return self._name @property def icon(self): return self._icon def seticon(self, icon): self._icon = icon @property def status(self): """Return file change status""" return self._status def setstatus(self, status): assert status in 'MARSC' self._status = status def __len__(self): return len(self._child) def __getitem__(self, name): return self._child[name] def addchild(self, name): if name not in self._child: self._nameindex.append(name) self._child[name] = self.__class__(name, parent=self) def __contains__(self, item): return item in self._child def at(self, index): return self._child[self._nameindex[index]] def index(self, name): return self._nameindex.index(name) def sort(self, reverse=False): """Sort the entries recursively; directories first""" for e in self._child.itervalues(): e.sort(reverse=reverse) self._nameindex.sort( key=lambda s: (not self[s], os.path.normcase(s)), reverse=reverse) class ManifestCompleter(QCompleter): """QCompleter for ManifestModel""" def splitPath(self, path): """ >>> c = ManifestCompleter() >>> c.splitPath(QString('foo/bar')) [u'foo', u'bar'] trailing slash appends extra '', so that QCompleter can descend to next level: >>> c.splitPath(QString('foo/')) [u'foo', u''] """ return unicode(path).split('/') def pathFromIndex(self, index): if not index.isValid(): return '' m = self.model() if not m: return '' return m.filePath(index) tortoisehg-2.10/tortoisehg/hgqt/chunks.py0000644000076400007640000007154012231647662017710 0ustar stevesteve# chunks.py - TortoiseHg patch/diff browser and editor # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import cStringIO import os, re from mercurial import util, patch, commands from mercurial import match as matchmod from hgext import record from tortoisehg.util import hglib from tortoisehg.util.patchctx import patchctx from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, qscilib, lexers, visdiff, revert, rejects from tortoisehg.hgqt import filelistmodel, filelistview, filedata, blockmatcher from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4 import Qsci # TODO # Add support for tools like TortoiseMerge that help resolve rejected chunks qsci = Qsci.QsciScintilla class ChunksWidget(QWidget): linkActivated = pyqtSignal(QString) showMessage = pyqtSignal(QString) chunksSelected = pyqtSignal(bool) fileSelected = pyqtSignal(bool) fileModelEmpty = pyqtSignal(bool) fileModified = pyqtSignal() contextmenu = None def __init__(self, repoagent, parent, multiselectable): QWidget.__init__(self, parent) self._repoagent = repoagent self.multiselectable = multiselectable self.currentFile = None layout = QVBoxLayout(self) layout.setSpacing(0) layout.setMargin(0) layout.setContentsMargins(2, 2, 2, 2) self.setLayout(layout) self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(False) self.layout().addWidget(self.splitter) repo = self._repoagent.rawRepo() self.filelist = filelistview.HgFileListView(repo, self, multiselectable) self.filelistmodel = filelistmodel.HgFileListModel(self) self.filelist.setModel(self.filelistmodel) self.filelist.setContextMenuPolicy(Qt.CustomContextMenu) self.filelist.customContextMenuRequested.connect(self.menuRequest) self.filelist.doubleClicked.connect(self.vdiff) self.fileListFrame = QFrame(self.splitter) self.fileListFrame.setFrameShape(QFrame.NoFrame) vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setMargin(0) vbox.addWidget(self.filelist) self.fileListFrame.setLayout(vbox) self.diffbrowse = DiffBrowser(self.splitter) self.diffbrowse.setFont(qtlib.getfont('fontdiff').font()) self.diffbrowse.showMessage.connect(self.showMessage) self.diffbrowse.linkActivated.connect(self.linkActivated) self.diffbrowse.chunksSelected.connect(self.chunksSelected) self.filelist.fileSelected.connect(self.displayFile) self.filelist.clearDisplay.connect(self.diffbrowse.clearDisplay) self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(1, 3) self.timerevent = self.startTimer(500) self._actions = {} for name, desc, icon, key, tip, cb in [ ('diff', _('Visual Diff'), 'visualdiff', 'Ctrl+D', _('View file changes in external diff tool'), self.vdiff), ('edit', _('Edit Local'), 'edit-file', 'Shift+Ctrl+L', _('Edit current file in working copy'), self.editCurrentFile), ('revert', _('Revert to Revision'), 'hg-revert', 'Shift+Ctrl+R', _('Revert file(s) to contents at this revision'), self.revertfile), ]: act = QAction(desc, self) if icon: act.setIcon(qtlib.geticon(icon)) if key: act.setShortcut(key) if tip: act.setStatusTip(tip) if cb: act.triggered.connect(cb) self._actions[name] = act self.addAction(act) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot(QPoint) def menuRequest(self, point): actionlist = ['diff', 'edit', 'revert'] if not self.contextmenu: menu = QMenu(self) for act in actionlist: menu.addAction(self._actions[act]) self.contextmenu = menu self.contextmenu.exec_(self.filelist.viewport().mapToGlobal(point)) def vdiff(self): filenames = self.getSelectedFiles() if len(filenames) == 0: return opts = {'change':self.ctx.rev()} dlg = visdiff.visualdiff(self.repo.ui, self.repo, filenames, opts) if dlg: dlg.exec_() def revertfile(self): filenames = self.getSelectedFiles() if len(filenames) == 0: return rev = self.ctx.rev() if rev is None: rev = self.ctx.p1().rev() dlg = revert.RevertDialog(self._repoagent, filenames, rev, self) dlg.exec_() dlg.deleteLater() def timerEvent(self, event): 'Periodic poll of currently displayed patch or working file' if not hasattr(self, 'filelist'): return ctx = self.ctx if ctx is None: return if isinstance(ctx, patchctx): path = ctx._path mtime = ctx._mtime elif self.currentFile: path = self.repo.wjoin(self.currentFile) mtime = self.mtime else: return try: if os.path.exists(path): newmtime = os.path.getmtime(path) if mtime != newmtime: self.mtime = newmtime self.refresh() except EnvironmentError: pass def runPatcher(self, fp, wfile, updatestate): ui = self.repo.ui.copy() class warncapt(ui.__class__): def warn(self, msg, *args, **opts): self.write(msg) ui.__class__ = warncapt ok = True repo = self.repo ui.pushbuffer() try: eolmode = ui.config('patch', 'eol', 'strict') if eolmode.lower() not in patch.eolmodes: eolmode = 'strict' else: eolmode = eolmode.lower() # 'updatestate' flag has no effect since hg 1.9 try: ret = patch.internalpatch(ui, repo, fp, 1, files=None, eolmode=eolmode, similarity=0) except ValueError: ret = -1 if ret < 0: ok = False self.showMessage.emit(_('Patch failed to apply')) except (patch.PatchError, EnvironmentError), err: ok = False self.showMessage.emit(hglib.tounicode(str(err))) rejfilere = re.compile(r'\b%s\.rej\b' % re.escape(wfile)) for line in ui.popbuffer().splitlines(): if rejfilere.search(line): if qtlib.QuestionMsgBox(_('Manually resolve rejected chunks?'), hglib.tounicode(line) + u'

    ' + _('Edit patched file and rejects?'), parent=self): dlg = rejects.RejectsDialog(repo.ui, repo.wjoin(wfile), self) if dlg.exec_() == QDialog.Accepted: ok = True break return ok def editCurrentFile(self): ctx = self.ctx if isinstance(ctx, patchctx): paths = [ctx._path] else: paths = self.getSelectedFiles() qtlib.editfiles(self.repo, paths, parent=self) def getSelectedFileAndChunks(self): chunks = self.diffbrowse.curchunks if chunks: dchunks = [c for c in chunks[1:] if c.selected] return self.currentFile, [chunks[0]] + dchunks else: return self.currentFile, [] def getSelectedFiles(self): return self.filelist.getSelectedFiles() def deleteSelectedChunks(self): 'delete currently selected chunks' repo = self.repo chunks = self.diffbrowse.curchunks dchunks = [c for c in chunks[1:] if c.selected] if not dchunks: self.showMessage.emit(_('No deletable chunks')) return ctx = self.ctx kchunks = [c for c in chunks[1:] if not c.selected] revertall = False if not kchunks: if isinstance(ctx, patchctx): revertmsg = _('Completely remove file from patch?') else: revertmsg = _('Revert all file changes?') revertall = qtlib.QuestionMsgBox(_('No chunks remain'), revertmsg) if isinstance(ctx, patchctx): repo.thgbackup(ctx._path) fp = util.atomictempfile(ctx._path, 'wb') buf = cStringIO.StringIO() try: if ctx._ph.comments: buf.write('\n'.join(ctx._ph.comments)) buf.write('\n\n') needsnewline = False for wfile in ctx._fileorder: if wfile == self.currentFile: if revertall: continue chunks[0].write(buf) for chunk in kchunks: chunk.write(buf) else: if buf.tell() and buf.getvalue()[-1] != '\n': buf.write('\n') for chunk in ctx._files[wfile]: chunk.write(buf) fp.write(buf.getvalue()) fp.close() finally: del fp ctx.invalidate() self.fileModified.emit() else: path = repo.wjoin(self.currentFile) if not os.path.exists(path): self.showMessage.emit(_('file has been deleted, refresh')) return if self.mtime != os.path.getmtime(path): self.showMessage.emit(_('file has been modified, refresh')) return repo.thgbackup(path) if revertall: commands.revert(repo.ui, repo, path, no_backup=True) else: wlock = repo.wlock() try: # atomictemp can preserve file permission wf = repo.wopener(self.currentFile, 'wb', atomictemp=True) wf.write(self.diffbrowse.origcontents) wf.close() fp = cStringIO.StringIO() chunks[0].write(fp) for c in kchunks: c.write(fp) fp.seek(0) self.runPatcher(fp, self.currentFile, False) finally: wlock.release() self.fileModified.emit() def mergeChunks(self, wfile, chunks): def isAorR(header): for line in header: if line.startswith('--- /dev/null'): return True if line.startswith('+++ /dev/null'): return True return False repo = self.repo ctx = self.ctx if isinstance(ctx, patchctx): if wfile in ctx._files: patchchunks = ctx._files[wfile] if isAorR(chunks[0].header) or isAorR(patchchunks[0].header): qtlib.InfoMsgBox(_('Unable to merge chunks'), _('Add or remove patches must be merged ' 'in the working directory')) return False # merge new chunks into existing chunks, sorting on start line newchunks = [chunks[0]] pidx = nidx = 1 while pidx < len(patchchunks) or nidx < len(chunks): if pidx == len(patchchunks): newchunks.append(chunks[nidx]) nidx += 1 elif nidx == len(chunks): newchunks.append(patchchunks[pidx]) pidx += 1 elif chunks[nidx].fromline < patchchunks[pidx].fromline: newchunks.append(chunks[nidx]) nidx += 1 else: newchunks.append(patchchunks[pidx]) pidx += 1 ctx._files[wfile] = newchunks else: # add file to patch ctx._files[wfile] = chunks ctx._fileorder.append(wfile) repo.thgbackup(ctx._path) fp = util.atomictempfile(ctx._path, 'wb') try: if ctx._ph.comments: fp.write('\n'.join(ctx._ph.comments)) fp.write('\n\n') for file in ctx._fileorder: for chunk in ctx._files[file]: chunk.write(fp) fp.close() ctx.invalidate() self.fileModified.emit() return True finally: del fp else: # Apply chunks to wfile repo.thgbackup(repo.wjoin(wfile)) fp = cStringIO.StringIO() for c in chunks: c.write(fp) fp.seek(0) wlock = repo.wlock() try: return self.runPatcher(fp, wfile, True) finally: wlock.release() def getFileList(self): return self.ctx.files() def removeFile(self, wfile): repo = self.repo ctx = self.ctx if isinstance(ctx, patchctx): repo.thgbackup(ctx._path) fp = util.atomictempfile(ctx._path, 'wb') try: if ctx._ph.comments: fp.write('\n'.join(ctx._ph.comments)) fp.write('\n\n') for file in ctx._fileorder: if file == wfile: continue for chunk in ctx._files[file]: chunk.write(fp) fp.close() finally: del fp ctx.invalidate() else: fullpath = repo.wjoin(wfile) repo.thgbackup(fullpath) wasadded = wfile in repo[None].added() try: commands.revert(repo.ui, repo, fullpath, rev='.', no_backup=True) if wasadded and os.path.exists(fullpath): os.unlink(fullpath) except EnvironmentError: qtlib.InfoMsgBox(_("Unable to remove"), _("Unable to remove file %s,\n" "permission denied") % hglib.tounicode(wfile)) self.fileModified.emit() def getChunksForFile(self, wfile): repo = self.repo ctx = self.ctx if isinstance(ctx, patchctx): if wfile in ctx._files: return ctx._files[wfile] else: return [] else: buf = cStringIO.StringIO() diffopts = patch.diffopts(repo.ui, {'git':True}) m = matchmod.exact(repo.root, repo.root, [wfile]) for p in patch.diff(repo, ctx.p1().node(), None, match=m, opts=diffopts): buf.write(p) buf.seek(0) chunks = record.parsepatch(buf) if chunks: header = chunks[0] return [header] + header.hunks else: return [] @pyqtSlot(QString, QString) def displayFile(self, file, status): if isinstance(file, (unicode, QString)): file = hglib.fromunicode(file) status = hglib.fromunicode(status) if file: self.currentFile = file path = self.repo.wjoin(file) if os.path.exists(path): self.mtime = os.path.getmtime(path) else: self.mtime = None self.diffbrowse.displayFile(file, status) self.fileSelected.emit(True) else: self.currentFile = None self.diffbrowse.clearDisplay() self.diffbrowse.clearChunks() self.fileSelected.emit(False) def setContext(self, ctx): self.diffbrowse.setContext(ctx) self.filelist.setContext(ctx) empty = len(ctx.files()) == 0 self.fileModelEmpty.emit(empty) self.fileSelected.emit(not empty) if empty: self.currentFile = None self.diffbrowse.clearDisplay() self.diffbrowse.clearChunks() self.diffbrowse.updateSummary() self.ctx = ctx for act in ['diff', 'revert']: self._actions[act].setEnabled(ctx.rev() is None) def refresh(self): ctx = self.ctx if isinstance(ctx, patchctx): # if patch mtime has not changed, it could return the same ctx ctx = self.repo.changectx(ctx._path) else: self.repo.thginvalidate() ctx = self.repo.changectx(ctx.node()) self.setContext(ctx) def loadSettings(self, qs, prefix): self.diffbrowse.loadSettings(qs, prefix) def saveSettings(self, qs, prefix): self.diffbrowse.saveSettings(qs, prefix) # DO NOT USE. Sadly, this does not work. class ElideLabel(QLabel): def __init__(self, text='', parent=None): QLabel.__init__(self, text, parent) def sizeHint(self): return super(ElideLabel, self).sizeHint() def paintEvent(self, event): p = QPainter() fm = QFontMetrics(self.font()) if fm.width(self.text()): # > self.contentsRect().width(): elided = fm.elidedText(self.text(), Qt.ElideLeft, self.rect().width(), 0) p.drawText(self.rect(), Qt.AlignTop | Qt.AlignRight | Qt.TextSingleLine, elided) else: super(ElideLabel, self).paintEvent(event) class DiffBrowser(QFrame): """diff browser""" linkActivated = pyqtSignal(QString) showMessage = pyqtSignal(QString) chunksSelected = pyqtSignal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.curchunks = [] self.countselected = 0 self._ctx = None self._lastfile = None self._status = None vbox = QVBoxLayout() vbox.setContentsMargins(0,0,0,0) vbox.setSpacing(0) self.setLayout(vbox) self.labelhbox = hbox = QHBoxLayout() hbox.setContentsMargins(0,0,0,0) hbox.setSpacing(2) self.layout().addLayout(hbox) self.filenamelabel = w = QLabel() self.filenamelabel.hide() hbox.addWidget(w) w.setWordWrap(True) f = w.textInteractionFlags() w.setTextInteractionFlags(f | Qt.TextSelectableByMouse) w.linkActivated.connect(self.linkActivated) self.searchbar = qscilib.SearchToolBar(hidable=True) self.searchbar.hide() self.searchbar.searchRequested.connect(self.find) self.searchbar.conditionChanged.connect(self.highlightText) guifont = qtlib.getfont('fontlist').font() self.sumlabel = QLabel() self.sumlabel.setFont(guifont) self.allbutton = QToolButton() self.allbutton.setFont(guifont) self.allbutton.setText(_('All', 'files')) self.allbutton.setShortcut(QKeySequence.SelectAll) self.allbutton.clicked.connect(self.selectAll) self.nonebutton = QToolButton() self.nonebutton.setFont(guifont) self.nonebutton.setText(_('None', 'files')) self.nonebutton.setShortcut(QKeySequence.New) self.nonebutton.clicked.connect(self.selectNone) self.actionFind = self.searchbar.toggleViewAction() self.actionFind.setIcon(qtlib.geticon('edit-find')) self.actionFind.setToolTip(_('Toggle display of text search bar')) qtlib.newshortcutsforstdkey(QKeySequence.Find, self, self.searchbar.show) self.diffToolbar = QToolBar(_('Diff Toolbar')) self.diffToolbar.setIconSize(QSize(16, 16)) self.diffToolbar.setStyleSheet(qtlib.tbstylesheet) self.diffToolbar.addAction(self.actionFind) hbox.addWidget(self.diffToolbar) hbox.addStretch(1) hbox.addWidget(self.sumlabel) hbox.addWidget(self.allbutton) hbox.addWidget(self.nonebutton) self.extralabel = w = QLabel() w.setWordWrap(True) w.linkActivated.connect(self.linkActivated) self.layout().addWidget(w) self.layout().addSpacing(2) w.hide() self._forceviewindicator = None self.sci = qscilib.Scintilla(self) self.sci.setReadOnly(True) self.sci.setUtf8(True) self.sci.installEventFilter(qscilib.KeyPressInterceptor(self)) self.sci.setCaretLineVisible(False) self.sci.setMarginType(1, qsci.SymbolMargin) self.sci.setMarginLineNumbers(1, False) self.sci.setMarginWidth(1, QFontMetrics(self.font()).width('XX')) self.sci.setMarginSensitivity(1, True) self.sci.marginClicked.connect(self.marginClicked) self._checkedpix = qtlib.getcheckboxpixmap(QStyle.State_On, Qt.gray, self) self.selected = self.sci.markerDefine(self._checkedpix, -1) self._uncheckedpix = qtlib.getcheckboxpixmap(QStyle.State_Off, Qt.gray, self) self.unselected = self.sci.markerDefine(self._uncheckedpix, -1) self.vertical = self.sci.markerDefine(qsci.VerticalLine, -1) self.divider = self.sci.markerDefine(qsci.Background, -1) self.selcolor = self.sci.markerDefine(qsci.Background, -1) self.sci.setMarkerBackgroundColor(QColor('#BBFFFF'), self.selcolor) self.sci.setMarkerBackgroundColor(QColor('#AAAAAA'), self.divider) mask = (1 << self.selected) | (1 << self.unselected) | \ (1 << self.vertical) | (1 << self.selcolor) | (1 << self.divider) self.sci.setMarginMarkerMask(1, mask) self.blksearch = blockmatcher.BlockList(self) self.blksearch.linkScrollBar(self.sci.verticalScrollBar()) self.blksearch.setVisible(False) hbox = QHBoxLayout() hbox.addWidget(self.sci) hbox.addWidget(self.blksearch) lexer = lexers.difflexer(self) self.sci.setLexer(lexer) self.layout().addLayout(hbox) self.layout().addWidget(self.searchbar) self.clearDisplay() def loadSettings(self, qs, prefix): self.sci.loadSettings(qs, prefix) def saveSettings(self, qs, prefix): self.sci.saveSettings(qs, prefix) def updateSummary(self): self.sumlabel.setText(_('Chunks selected: %d / %d') % ( self.countselected, len(self.curchunks[1:]))) self.chunksSelected.emit(self.countselected > 0) @pyqtSlot() def selectAll(self): for chunk in self.curchunks[1:]: if not chunk.selected: self.sci.markerDelete(chunk.mline, -1) self.sci.markerAdd(chunk.mline, self.selected) chunk.selected = True self.countselected += 1 for i in xrange(*chunk.lrange): self.sci.markerAdd(i, self.selcolor) self.updateSummary() @pyqtSlot() def selectNone(self): for chunk in self.curchunks[1:]: if chunk.selected: self.sci.markerDelete(chunk.mline, -1) self.sci.markerAdd(chunk.mline, self.unselected) chunk.selected = False self.countselected -= 1 for i in xrange(*chunk.lrange): self.sci.markerDelete(i, self.selcolor) self.updateSummary() @pyqtSlot(int, int, Qt.KeyboardModifiers) def marginClicked(self, margin, line, modifiers): for chunk in self.curchunks[1:]: if line >= chunk.lrange[0] and line < chunk.lrange[1]: self.toggleChunk(chunk) self.updateSummary() return def toggleChunk(self, chunk): self.sci.markerDelete(chunk.mline, -1) if chunk.selected: self.sci.markerAdd(chunk.mline, self.unselected) chunk.selected = False self.countselected -= 1 for i in xrange(*chunk.lrange): self.sci.markerDelete(i, self.selcolor) else: self.sci.markerAdd(chunk.mline, self.selected) chunk.selected = True self.countselected += 1 for i in xrange(*chunk.lrange): self.sci.markerAdd(i, self.selcolor) def setContext(self, ctx): self._ctx = ctx self.sci.setTabWidth(ctx._repo.tabwidth) def clearDisplay(self): self.sci.clear() self.filenamelabel.setText(' ') self.extralabel.hide() self.blksearch.clear() def clearChunks(self): self.curchunks = [] self.countselected = 0 self.updateSummary() def _setupForceViewIndicator(self): if not self._forceviewindicator: self._forceviewindicator = self.sci.indicatorDefine(self.sci.PlainIndicator) self.sci.setIndicatorDrawUnder(True, self._forceviewindicator) self.sci.setIndicatorForegroundColor( QColor('blue'), self._forceviewindicator) # delay until next event-loop in order to complete mouse release self.sci.SCN_INDICATORRELEASE.connect(self.forceDisplayFile, Qt.QueuedConnection) def forceDisplayFile(self): if self.curchunks: return self.sci.setText(_('Please wait while the file is opened ...')) QTimer.singleShot(10, lambda: self.displayFile(self._lastfile, self._status, force=True)) def displayFile(self, filename, status, force=False): self._status = status self.clearDisplay() if filename == self._lastfile: reenable = [(c.fromline, len(c.before)) for c in self.curchunks[1:]\ if c.selected] else: reenable = [] self._lastfile = filename self.clearChunks() fd = filedata.FileData(self._ctx, None, filename, status, force=force) if fd.elabel: self.extralabel.setText(fd.elabel) self.extralabel.show() else: self.extralabel.hide() self.filenamelabel.setText(fd.flabel) if not fd.isValid() or not fd.diff: if fd.error is None: self.sci.clear() return self.sci.setText(fd.error) forcedisplaymsg = filedata.forcedisplaymsg linkstart = fd.error.find(forcedisplaymsg) if linkstart >= 0: # add the link to force to view the data anyway self._setupForceViewIndicator() self.sci.fillIndicatorRange( 0, linkstart, 0, linkstart+len(forcedisplaymsg), self._forceviewindicator) return elif type(self._ctx.rev()) is str: chunks = self._ctx._files[filename] else: header = record.parsepatch(cStringIO.StringIO(fd.diff))[0] chunks = [header] + header.hunks utext = [] for chunk in chunks[1:]: buf = cStringIO.StringIO() chunk.selected = False chunk.write(buf) chunk.lines = buf.getvalue().splitlines() utext += [hglib.tounicode(l) for l in chunk.lines] utext.append('') self.sci.setText(u'\n'.join(utext)) start = 0 self.sci.markerDeleteAll(-1) for chunk in chunks[1:]: chunk.lrange = (start, start+len(chunk.lines)) chunk.mline = start if start: self.sci.markerAdd(start-1, self.divider) for i in xrange(0,len(chunk.lines)): if start + i == chunk.mline: self.sci.markerAdd(chunk.mline, self.unselected) else: self.sci.markerAdd(start+i, self.vertical) start += len(chunk.lines) + 1 self.origcontents = fd.olddata self.countselected = 0 self.curchunks = chunks for c in chunks[1:]: if (c.fromline, len(c.before)) in reenable: self.toggleChunk(c) self.updateSummary() @pyqtSlot(unicode, bool, bool, bool) def find(self, exp, icase=True, wrap=False, forward=True): self.sci.find(exp, icase, wrap, forward) @pyqtSlot(unicode, bool) def highlightText(self, match, icase=False): self._lastSearch = match, icase self.sci.highlightText(match, icase) blk = self.blksearch blk.clear() blk.setUpdatesEnabled(False) blk.clear() for l in self.sci.highlightLines: blk.addBlock('s', l, l + 1) blk.setVisible(bool(match)) blk.setUpdatesEnabled(True) tortoisehg-2.10/tortoisehg/hgqt/revert.py0000644000076400007640000000772712231647662017732 0ustar stevesteve# revert.py - File revert dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial.node import nullid from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, cmdui, qtlib class RevertDialog(QDialog): def __init__(self, repoagent, wfiles, rev, parent): super(RevertDialog, self).__init__(parent) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self.setWindowTitle(_('Revert - %s') % repo.displayname) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self.wfiles = [repo.wjoin(wfile) for wfile in wfiles] self.setLayout(QVBoxLayout()) if len(wfile) == 1: lblText = _('Revert %s to its contents' ' at the following revision?') % ( hglib.tounicode(wfiles[0])) else: lblText = _('Revert %d files to their contents' ' at the following revision?') % ( len(wfiles)) lbl = QLabel(lblText) self.layout().addWidget(lbl) self._addRevertTargetCombo(rev) self.allchk = QCheckBox(_('Revert all files to this revision')) self.layout().addWidget(self.allchk) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Ok|BB.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) self.bbox = bbox def _addRevertTargetCombo(self, rev): if rev is None: raise ValueError('Cannot revert to working directory') self.revcombo = QComboBox() revnames = ['revision %d' % rev] repo = self._repoagent.rawRepo() ctx = repo[rev] parents = ctx.parents()[:2] if len(parents) == 1: parentdesctemplate = ("revision %d's parent (i.e. revision %d)",) else: parentdesctemplate = ( _("revision %d's first parent (i.e. revision %d)"), _("revision %d's second parent (i.e. revision %d)"), ) for n, pctx in enumerate(parents): if pctx.node() == nullid: revdesc = _('null revision (i.e. remove file(s))') else: revdesc = parentdesctemplate[n] % (rev, pctx.rev()) revnames.append(revdesc) self.revcombo.addItems(revnames) reverttargets = [ctx] + parents for n, ctx in enumerate(reverttargets): self.revcombo.setItemData(n, ctx.hex()) self.layout().addWidget(self.revcombo) def accept(self): rev = self.revcombo.itemData(self.revcombo.currentIndex()).toString() if self.allchk.isChecked(): if not qtlib.QuestionMsgBox(_('Confirm Revert'), _('Reverting all files will discard changes and ' 'leave affected files in a modified state.
    ' '
    Are you sure you want to use revert?

    ' '(use update to checkout another revision)'), parent=self): return cmdline = hglib.buildcmdargs('revert', all=True, rev=rev) else: files = map(hglib.tounicode, self.wfiles) cmdline = hglib.buildcmdargs('revert', rev=rev, *files) self.bbox.button(QDialogButtonBox.Ok).setEnabled(False) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onCommandFinished) @pyqtSlot(int) def _onCommandFinished(self, ret): if ret == 0: self.reject() else: cmdui.errorMessageBox(self._cmdsession, self) tortoisehg-2.10/tortoisehg/hgqt/hgemail.py0000644000076400007640000003704512231647662020025 0ustar stevesteve# hgemail.py - TortoiseHg's dialog for sending patches via email # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os, tempfile, re from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error, util from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdui, lexers, qtlib from tortoisehg.hgqt.hgemail_ui import Ui_EmailDialog class EmailDialog(QDialog): """Dialog for sending patches via email""" def __init__(self, repoagent, revs, parent=None, outgoing=False, outgoingrevs=None): """Create EmailDialog for the given repo and revs :revs: List of revisions to be sent. :outgoing: Enable outgoing bundle support. You also need to set outgoing revisions to `revs`. :outgoingrevs: Target revision of outgoing bundle. (Passed as `hg email --bundle --rev {rev}`) """ super(EmailDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self._repoagent = repoagent self._outgoing = outgoing self._outgoingrevs = outgoingrevs or [] self._qui = Ui_EmailDialog() self._qui.setupUi(self) self._initchangesets(revs) self._initpreviewtab() self._initenvelopebox() self._qui.bundle_radio.toggled.connect(self._updateforms) self._qui.attach_check.toggled.connect(self._updateattachmodes) self._qui.inline_check.toggled.connect(self._updateattachmodes) self._initintrobox() self._readhistory() self._filldefaults() self._updateforms() self._updateattachmodes() self._readsettings() QShortcut(QKeySequence('CTRL+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) def closeEvent(self, event): self._writesettings() super(EmailDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('email/geom').toByteArray()) self._qui.intro_changesets_splitter.restoreState( s.value('email/intro_changesets_splitter').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('email/geom', self.saveGeometry()) s.setValue('email/intro_changesets_splitter', self._qui.intro_changesets_splitter.saveState()) def _readhistory(self): s = QSettings() for k in ('to', 'cc', 'from', 'flag', 'subject'): w = getattr(self._qui, '%s_edit' % k) w.addItems(s.value('email/%s_history' % k).toStringList()) w.setCurrentIndex(-1) # unselect for k in ('body', 'attach', 'inline', 'diffstat'): w = getattr(self._qui, '%s_check' % k) w.setChecked(s.value('email/%s' % k).toBool()) def _writehistory(self): def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s = QSettings() for k in ('to', 'cc', 'from', 'flag', 'subject'): w = getattr(self._qui, '%s_edit' % k) s.setValue('email/%s_history' % k, list(itercombo(w))[:10]) for k in ('body', 'attach', 'inline', 'diffstat'): w = getattr(self._qui, '%s_check' % k) s.setValue('email/%s' % k, w.isChecked()) def _initchangesets(self, revs): self._changesets = _ChangesetsModel(self._repo, revs=revs or list(self._repo), selectedrevs=revs, parent=self) self._changesets.dataChanged.connect(self._updateforms) self._qui.changesets_view.setModel(self._changesets) @property def _repo(self): return self._repoagent.rawRepo() @property def _ui(self): return self._repo.ui @property def _revs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs def _filldefaults(self): """Fill form by default values""" def getfromaddr(ui): """Get sender address in the same manner as patchbomb""" addr = ui.config('email', 'from') or ui.config('patchbomb', 'from') if addr: return addr try: return ui.username() except error.Abort: return '' self._qui.to_edit.setEditText( hglib.tounicode(self._ui.config('email', 'to', ''))) self._qui.cc_edit.setEditText( hglib.tounicode(self._ui.config('email', 'cc', ''))) self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui))) self.setdiffformat(self._ui.configbool('diff', 'git') and 'git' or 'hg') def setdiffformat(self, format): """Set diff format, 'hg', 'git' or 'plain'""" try: radio = getattr(self._qui, '%spatch_radio' % format) except AttributeError: raise ValueError('unknown diff format: %r' % format) radio.setChecked(True) def getdiffformat(self): """Selected diff format""" for e in self._qui.patch_frame.children(): m = re.match(r'(\w+)patch_radio', str(e.objectName())) if m and e.isChecked(): return m.group(1) return 'hg' def getextraopts(self): """Dict of extra options""" opts = {} for e in self._qui.extra_frame.children(): m = re.match(r'(\w+)_check', str(e.objectName())) if m: opts[m.group(1)] = e.isChecked() return opts def _patchbombopts(self, **opts): """Generate opts for patchbomb by form values""" def headertext(s): # QLineEdit may contain newline character return re.sub(r'\s', ' ', unicode(s)) opts['to'] = [headertext(self._qui.to_edit.currentText())] opts['cc'] = [headertext(self._qui.cc_edit.currentText())] opts['from'] = headertext(self._qui.from_edit.currentText()) opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text()) opts['flag'] = [headertext(self._qui.flag_edit.currentText())] if self._qui.bundle_radio.isChecked(): assert self._outgoing # only outgoing bundle is supported opts['rev'] = self._outgoingrevs opts['bundle'] = True else: opts['rev'] = self._revs def diffformat(): n = self.getdiffformat() if n == 'hg': return {} else: return {n: True} opts.update(diffformat()) opts.update(self.getextraopts()) def writetempfile(s): fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_', dir=qtlib.gettempdir()) try: os.write(fd, s) return hglib.tounicode(fname) finally: os.close(fd) opts['intro'] = self._qui.writeintro_check.isChecked() if opts['intro']: opts['subject'] = headertext(self._qui.subject_edit.currentText()) opts['desc'] = writetempfile( hglib.fromunicode(self._qui.body_edit.toPlainText())) return opts def _isvalid(self): """Filled all required values?""" for e in ('to_edit', 'from_edit'): if not getattr(self._qui, e).currentText(): return False if (self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.currentText()): return False if not self._revs: return False return True @pyqtSlot() def _updateforms(self): """Update availability of form widgets""" valid = self._isvalid() self._qui.send_button.setEnabled(valid) self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid) self._qui.writeintro_check.setEnabled(not self._introrequired()) self._qui.bundle_radio.setEnabled( self._outgoing and self._changesets.isselectedall()) self._changesets.setReadOnly(self._qui.bundle_radio.isChecked()) if self._qui.bundle_radio.isChecked(): # workaround to disable preview for outgoing bundle because it # may freeze main thread self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False) if self._introrequired(): self._qui.writeintro_check.setChecked(True) #@pyqtSlot() def _updateattachmodes(self): """Update checkboxes to select the embedding style of the patch""" attachmodes = [self._qui.attach_check, self._qui.inline_check] body = self._qui.body_check # --attach and --inline are exclusive if self.sender() in attachmodes and self.sender().isChecked(): for w in attachmodes: if w is not self.sender(): w.setChecked(False) # --body is mandatory if no attach modes are specified body.setEnabled(util.any(w.isChecked() for w in attachmodes)) if not body.isEnabled(): body.setChecked(True) def _initenvelopebox(self): for e in ('to_edit', 'from_edit'): getattr(self._qui, e).editTextChanged.connect(self._updateforms) def accept(self): hglib.loadextension(self._ui, 'patchbomb') opts = self._patchbombopts() cmdline = hglib.buildcmdargs('email', **opts) cmd = cmdui.CmdSessionDialog(self) cmd.setWindowTitle(_('Sending Email')) cmd.setLogVisible(False) cmd.setSession(self._repoagent.runCommand(cmdline, self)) if cmd.exec_(): self._writehistory() def _initintrobox(self): self._qui.intro_box.hide() # hidden by default self._qui.subject_edit.editTextChanged.connect(self._updateforms) self._qui.writeintro_check.toggled.connect(self._updateforms) def _introrequired(self): """Is intro message required?""" return len(self._revs) > 1 or self._qui.bundle_radio.isChecked() def _initpreviewtab(self): def initqsci(w): w.setUtf8(True) w.setReadOnly(True) w.setMarginWidth(1, 0) # hide area for line numbers self.lexer = lex = lexers.difflexer(self) fh = qtlib.getfont('fontdiff') fh.changed.connect(self.forwardFont) lex.setFont(fh.font()) w.setLexer(lex) # TODO: better way to setup diff lexer initqsci(self._qui.preview_edit) self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab) self._refreshpreviewtab(self._qui.main_tabs.currentIndex()) def forwardFont(self, font): if self.lexer: self.lexer.setFont(font) @pyqtSlot(int) def _refreshpreviewtab(self, index): """Generate preview text if current tab is preview""" if self._previewtabindex() != index: return self._qui.preview_edit.clear() opts = self._patchbombopts(test=True) # TODO: fix hgext.patchbomb's implementation instead if 'PAGER' in os.environ: del os.environ['PAGER'] cmdline = hglib.buildcmdargs('email', **opts) sess = self._repoagent.runCommand(cmdline) sess.outputReceived.connect(self._capturepreview) @pyqtSlot(unicode, unicode) def _capturepreview(self, msg, label): if label: return # ignore ui or control message self._qui.preview_edit.append(msg) def _previewtabindex(self): """Index of preview tab""" return self._qui.main_tabs.indexOf(self._qui.preview_tab) @pyqtSlot() def on_settings_button_clicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='email.from').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self._repo.invalidateui() # force reloading config immediately self._filldefaults() @pyqtSlot() def on_selectall_button_clicked(self): self._changesets.selectAll() @pyqtSlot() def on_selectnone_button_clicked(self): self._changesets.selectNone() # TODO: use component of log viewer? class _ChangesetsModel(QAbstractTableModel): _COLUMNS = [('rev', lambda ctx: '%d:%s' % (ctx.rev(), ctx)), ('author', lambda ctx: hglib.username(ctx.user())), ('date', lambda ctx: util.shortdate(ctx.date())), ('description', lambda ctx: ctx.longsummary())] def __init__(self, repo, revs, selectedrevs, parent=None): super(_ChangesetsModel, self).__init__(parent) self._repo = repo self._revs = list(reversed(sorted(revs))) self._selectedrevs = set(selectedrevs) self._readonly = False @property def revs(self): return self._revs @property def selectedrevs(self): """Return the list of selected revisions""" return list(sorted(self._selectedrevs)) def isselectedall(self): return len(self._revs) == len(self._selectedrevs) def data(self, index, role): if not index.isValid(): return QVariant() rev = self._revs[index.row()] if index.column() == 0 and role == Qt.CheckStateRole: return rev in self._selectedrevs and Qt.Checked or Qt.Unchecked if role == Qt.DisplayRole: coldata = self._COLUMNS[index.column()][1] return QVariant(hglib.tounicode(coldata(self._repo.changectx(rev)))) return QVariant() def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or self._readonly: return False rev = self._revs[index.row()] if index.column() == 0 and role == Qt.CheckStateRole: origvalue = rev in self._selectedrevs if value == Qt.Checked: self._selectedrevs.add(rev) else: self._selectedrevs.remove(rev) if origvalue != (rev in self._selectedrevs): self.dataChanged.emit(index, index) return True return False def setReadOnly(self, readonly): self._readonly = readonly def flags(self, index): v = super(_ChangesetsModel, self).flags(index) if index.column() == 0 and not self._readonly: return Qt.ItemIsUserCheckable | v else: return v def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self._revs) def columnCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # no child return len(self._COLUMNS) def headerData(self, section, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() return QVariant(self._COLUMNS[section][0].capitalize()) def selectAll(self): self._selectedrevs = set(self._revs) self.updateAll() def selectNone(self): self._selectedrevs = set() self.updateAll() def updateAll(self): first = self.createIndex(0, 0) last = self.createIndex(len(self._revs) - 1, 0) self.dataChanged.emit(first, last) tortoisehg-2.10/tortoisehg/hgqt/cmdcore.py0000644000076400007640000003065412231647662020032 0ustar stevesteve# cmdcore.py - run Mercurial commands in a separate thread or process # # Copyright 2010 Yuki KODAMA # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os, signal, time from PyQt4.QtCore import QIODevice, QObject, QProcess, QTimer from PyQt4.QtCore import pyqtSignal, pyqtSlot from tortoisehg.util import hglib, paths from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import thread class CmdProc(QObject): 'Run mercurial command in separate process' started = pyqtSignal() commandFinished = pyqtSignal(int) outputReceived = pyqtSignal(unicode, unicode) # progress is not supported but needed to be a worker class progressReceived = pyqtSignal(unicode, object, unicode, unicode, object) def __init__(self, cmdline, parent=None): super(CmdProc, self).__init__(parent) self.cmdline = cmdline self._proc = proc = QProcess(self) proc.started.connect(self.started) proc.finished.connect(self.commandFinished) proc.readyReadStandardOutput.connect(self._stdout) proc.readyReadStandardError.connect(self._stderr) proc.error.connect(self._handleerror) def start(self): fullcmdline = paths.get_hg_command() + self.cmdline self._proc.start(fullcmdline[0], fullcmdline[1:], QIODevice.ReadOnly) def abort(self): if not self.isRunning(): return if os.name == 'nt': # TODO: do not TerminateProcess(); send CTRL_C_EVENT in some way self._proc.close() else: os.kill(int(self._proc.pid()), signal.SIGINT) def isRunning(self): return self._proc.state() != QProcess.NotRunning def _handleerror(self, error): if error == QProcess.FailedToStart: self.outputReceived.emit(_('failed to start command\n'), 'ui.error') self.commandFinished.emit(-1) elif error != QProcess.Crashed: self.outputReceived.emit(_('error while running command\n'), 'ui.error') def _stdout(self): data = self._proc.readAllStandardOutput().data() self.outputReceived.emit(hglib.tounicode(data), '') def _stderr(self): data = self._proc.readAllStandardError().data() self.outputReceived.emit(hglib.tounicode(data), 'ui.error') def _quotecmdarg(arg): # only for display; no use to construct command string for os.system() if not arg or ' ' in arg or '\\' in arg or '"' in arg: return '"%s"' % arg.replace('"', '\\"') else: return arg def _prettifycmdline(cmdline): r"""Build pretty command-line string for display >>> _prettifycmdline(['--repository', 'foo', 'status']) 'status' >>> _prettifycmdline(['--cwd', 'foo', 'resolve', '--', '--repository']) 'resolve -- --repository' >>> _prettifycmdline(['log', 'foo\\bar', '', 'foo bar', 'foo"bar']) 'log "foo\\bar" "" "foo bar" "foo\\"bar"' """ try: argcount = cmdline.index('--') except ValueError: argcount = len(cmdline) printables = [] pos = 0 while pos < argcount: if cmdline[pos] in ('-R', '--repository', '--cwd'): pos += 2 else: printables.append(cmdline[pos]) pos += 1 printables.extend(cmdline[argcount:]) return ' '.join(_quotecmdarg(e) for e in printables) class CmdSession(QObject): """Run Mercurial commands in a background thread or process""" commandFinished = pyqtSignal(int) outputReceived = pyqtSignal(unicode, unicode) progressReceived = pyqtSignal(unicode, object, unicode, unicode, object) # TODO: instead of useproc, run() will receive worker instance def __init__(self, cmdlines, parent=None, display=None, useproc=False): super(CmdSession, self).__init__(parent) self._worker = None self._queue = list(cmdlines) self._display = display self._useproc = useproc self._abortbyuser = False self._erroroutputs = [] self._warningoutputs = [] self._exitcode = 0 ### Public Methods ### def run(self): '''Execute Mercurial command''' if self.isRunning() or not self._queue: return if self._abortbyuser: # -1 instead of 255 for compatibility with CmdThread self._finish(-1) return self._runNext() def abort(self): '''Cancel running Mercurial command''' if self.isRunning(): self._worker.abort() del self._queue[:] self._abortbyuser = True elif not self.isFinished(): # don't abort immediately because this hasn't started yet self._abortbyuser = True def isAborted(self): """True if commands have finished by user abort""" return self.isFinished() and self._abortbyuser def isFinished(self): """True if all pending commands have finished or been aborted""" return not self._queue and not self.isRunning() def isRunning(self): """True if a command is running; False if finished or not started yet""" # keep "running" until just before emitting commandFinished. if worker # is QThread, isRunning() is cleared earlier than _onCommandFinished, # because inter-thread signal is queued. return bool(self._worker) def errorString(self): """Error message received in the last command""" if self._abortbyuser: return _('Terminated by user') else: return ''.join(self._erroroutputs).rstrip() def warningString(self): """Warning message received in the last command""" return ''.join(self._warningoutputs).rstrip() def exitCode(self): """Integer return code of the last command""" return self._exitcode ### Private Method ### def _createWorker(self, cmdline): cmdline = map(hglib.fromunicode, cmdline) if self._useproc: return CmdProc(cmdline, self) else: return thread.CmdThread(cmdline, self) def _runNext(self): cmdline = self._queue.pop(0) if not self._display: self._display = _prettifycmdline(cmdline) self._worker = self._createWorker(cmdline) self._worker.started.connect(self._onCommandStarted) self._worker.commandFinished.connect(self._onCommandFinished) self._worker.outputReceived.connect(self.outputReceived) self._worker.outputReceived.connect(self._captureOutput) self._worker.progressReceived.connect(self.progressReceived) self._abortbyuser = False del self._erroroutputs[:] del self._warningoutputs[:] self._worker.start() def _finish(self, ret): del self._queue[:] self._worker = None self._exitcode = ret self.commandFinished.emit(ret) ### Signal Handlers ### @pyqtSlot() def _onCommandStarted(self): cmd = '%% hg %s\n' % self._display self.outputReceived.emit(cmd, 'control') @pyqtSlot(int) def _onCommandFinished(self, ret): if ret == -1: if self._abortbyuser: msg = _('[command terminated by user %s]') else: msg = _('[command interrupted %s]') elif ret: msg = _('[command returned code %d %%s]') % ret else: msg = _('[command completed successfully %s]') self.outputReceived.emit(msg % time.asctime() + '\n', 'control') self._display = None self._worker.setParent(None) # assist gc if ret != 0 or not self._queue: self._finish(ret) else: self._runNext() @pyqtSlot(unicode, unicode) def _captureOutput(self, msg, label): if not label: return # fast path labelset = unicode(label).split() # typically ui.error is sent only once at end if 'ui.error' in labelset: self._erroroutputs.append(unicode(msg)) elif 'ui.warning' in labelset: self._warningoutputs.append(unicode(msg)) def nullCmdSession(): """Finished CmdSession object which can be used as the initial value >>> sess = nullCmdSession() >>> sess.isFinished(), sess.isRunning(), sess.isAborted() (True, False, False) >>> sess.abort() # should not change flags >>> sess.isFinished(), sess.isRunning(), sess.isAborted() (True, False, False) """ return CmdSession([]) # TODO: remove with increment/decrementBusyCount class _RunningCmdSession(CmdSession): def abort(self): pass def isRunning(self): return True def runningCmdSession(): """CmdSession object which can be used as a stub for busy count >>> sess = runningCmdSession() >>> sess.isFinished(), sess.isRunning(), sess.isAborted() (False, True, False) >>> sess.run() # do nothing >>> sess.isFinished(), sess.isRunning(), sess.isAborted() (False, True, False) >>> sess.abort() # do nothing >>> sess.isFinished(), sess.isRunning(), sess.isAborted() (False, True, False) """ return _RunningCmdSession([]) class CmdAgent(QObject): """Manage requests of Mercurial commands""" busyChanged = pyqtSignal(bool) outputReceived = pyqtSignal(unicode, unicode) progressReceived = pyqtSignal(unicode, object, unicode, unicode, object) def __init__(self, parent=None): super(CmdAgent, self).__init__(parent) self._cwd = None self._sessqueue = [] # [active, waiting...] self._runlater = QTimer(self, interval=0, singleShot=True) self._runlater.timeout.connect(self._runNextSession) def workingDirectory(self): return self._cwd def setWorkingDirectory(self, cwd): self._cwd = unicode(cwd) def isBusy(self): return bool(self._sessqueue) def _enqueueSession(self, sess): self._sessqueue.append(sess) if len(self._sessqueue) == 1: self.busyChanged.emit(self.isBusy()) # make sure no command signals emitted in the current context self._runlater.start() def _dequeueSession(self, sess): # TODO: can't sessqueue.pop(0) due to incrementBusyCount hack self._sessqueue.remove(sess) if self._sessqueue: # make sure client can receive commandFinished before next session self._runlater.start() else: self._runlater.stop() self.busyChanged.emit(self.isBusy()) def runCommand(self, cmdline, parent=None, display=None, worker=None): """Executes a single Mercurial command asynchronously and returns new CmdSession object""" return self.runCommandSequence([cmdline], parent=parent, display=display, worker=worker) def runCommandSequence(self, cmdlines, parent=None, display=None, worker=None): """Executes a series of Mercurial commands asynchronously and returns new CmdSession object which will provide notification signals. CmdSession object will be disowned on command finished, even if parent is specified. If one of the preceding command exits with non-zero status, the following commands won't be executed. """ if self._cwd: # TODO: pass to CmdSession so that CmdProc can use it directly cmdlines = [['--cwd', self._cwd] + list(xs) for xs in cmdlines] useproc = worker == 'proc' if not parent: parent = self sess = CmdSession(cmdlines, parent, display, useproc) sess.commandFinished.connect(self._onCommandFinished) sess.outputReceived.connect(self.outputReceived) sess.progressReceived.connect(self.progressReceived) self._enqueueSession(sess) return sess def abortCommands(self): """Abort running and queued commands; all command sessions will emit commandFinished in order""" for sess in self._sessqueue[:]: sess.abort() @pyqtSlot() def _runNextSession(self): sess = self._sessqueue[0] sess.run() @pyqtSlot() def _onCommandFinished(self): sess = self._sessqueue[0] if isinstance(self.sender(), CmdSession): # avoid bug of PyQt 4.7.3 assert sess is self.sender() sess.setParent(None) self._dequeueSession(sess) tortoisehg-2.10/tortoisehg/hgqt/hgemail.ui0000644000076400007640000004610012135406414017771 0ustar stevesteve EmailDialog 0 0 660 519 Email true 0 false false false Edit QFormLayout::ExpandingFieldsGrow To: to_edit 0 0 true QComboBox::InsertAtTop Cc: cc_edit 0 0 true QComboBox::InsertAtTop From: from_edit 0 0 true QComboBox::InsertAtTop In-Reply-To: inreplyto_edit Message identifier to reply to, for threading Flag: flag_edit 0 0 true QComboBox::InsertAtTop 0 0 QFrame::NoFrame QFrame::Raised Hg patches (as generated by export command) are compatible with most patch programs. They include a header which contains the most important changeset metadata. Send changesets as Hg patches Git patches can describe binary files, copies, and permission changes, but recipients may not be able to use them if they are not using git or Mercurial. Use extended (git) patch format Stripping Mercurial header removes username and parent information. Only useful if recipient is not using Mercurial (and does not like to see the headers). Plain, do not prepend Hg header Bundles store complete changesets in binary form. Upstream users can pull from them. This is the safest way to send changes to recipient Mercurial users. Send single binary bundle, not patches QFrame::NoFrame QFrame::Raised send patches as part of the email body body true true send patches as attachments attach send patches as inline attachments inline add diffstat output to messages diffstat Qt::Horizontal 40 20 Patch series description is sent in initial summary email with [PATCH 0 of N] subject. It should describe the effects of the entire patch series. When emailing a bundle, these fields make up the message subject and body. Flags is a comma separated list of tags which are inserted into the message subject prefix. Write patch series (bundle) description Qt::Vertical Subject: subject_edit 0 0 true QComboBox::InsertAtTop Monospace Changesets 0 false false Select &All Select &None Qt::Horizontal 40 20 Preview &Settings false Qt::Horizontal 25 19 false Send &Email false true &Close true QsciScintilla QFrame

    Qsci/qsciscintilla.h
    main_tabs to_edit cc_edit from_edit inreplyto_edit flag_edit hgpatch_radio gitpatch_radio plainpatch_radio bundle_radio body_check attach_check inline_check diffstat_check writeintro_check subject_edit body_edit changesets_view send_button preview_edit settings_button writeintro_check toggled(bool) intro_box setVisible(bool) 129 222 133 252 send_button clicked() EmailDialog accept() 641 501 528 506 close_button clicked() EmailDialog close() 641 501 528 506 writeintro_check toggled(bool) subject_edit setFocus() 86 214 177 244 tortoisehg-2.10/tortoisehg/hgqt/workbench.py0000644000076400007640000014513512235634453020377 0ustar stevesteve# workbench.py - main TortoiseHg Window # # Copyright (C) 2007-2010 Logilab. All rights reserved. # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. """ Main Qt4 application for TortoiseHg """ import os import sys from mercurial.error import RepoError from tortoisehg.util import paths, hglib from tortoisehg.hgqt import cmdui, qtlib, mq, serve from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt.repowidget import RepoWidget from tortoisehg.hgqt.reporegistry import RepoRegistryView from tortoisehg.hgqt.logcolumns import ColumnSelectDialog from tortoisehg.hgqt.docklog import LogDockWidget from tortoisehg.hgqt.settings import SettingsDialog from PyQt4.QtCore import * from PyQt4.QtGui import * class ThgTabBar(QTabBar): def mouseReleaseEvent(self, event): if event.button() == Qt.MidButton: self.tabCloseRequested.emit(self.tabAt(event.pos())) super(ThgTabBar, self).mouseReleaseEvent(event) class Workbench(QMainWindow): """hg repository viewer/browser application""" finished = pyqtSignal(int) def __init__(self, ui, repomanager): QMainWindow.__init__(self) self.progressDialog = QProgressDialog( 'TortoiseHg - Initializing Workbench', QString(), 0, 100) self.progressDialog.setAutoClose(False) self.ui = ui self._repomanager = repomanager self._repomanager.configChanged.connect(self._setupUrlComboIfCurrent) self._repomanager.repositoryDestroyed.connect(self.closeRepo) self.setupUi() self.reporegistry = rr = RepoRegistryView(repomanager, self) rr.setObjectName('RepoRegistryView') rr.showMessage.connect(self.showMessage) rr.openRepo.connect(self.openRepo) rr.removeRepo.connect(self.closeRepo) rr.progressReceived.connect(self.progress) self._repomanager.repositoryChanged.connect(rr.scanRepo) rr.hide() self.addDockWidget(Qt.LeftDockWidgetArea, rr) self.mqpatches = p = mq.MQPatchesWidget(self) p.setObjectName('MQPatchesWidget') p.patchSelected.connect(self.gotorev) p.hide() self.addDockWidget(Qt.LeftDockWidgetArea, p) self.log = LogDockWidget(repomanager, self) self.log.setObjectName('Log') self.log.progressReceived.connect(self.statusbar.progress) self.log.hide() self.addDockWidget(Qt.BottomDockWidgetArea, self.log) self._setupActions() self._repoTabTitleChangedMapper = mapper = QSignalMapper(self) mapper.mapped[QWidget].connect(self._updateRepoTabTitle) self._repoTabBusyIconChangedMapper = mapper = QSignalMapper(self) mapper.mapped[QWidget].connect(self._updateRepoTabIcon) self.restoreSettings() self.repoTabChanged() self.setAcceptDrops(True) if os.name == 'nt': # Allow CTRL+Q to close Workbench on Windows QShortcut(QKeySequence('CTRL+Q'), self, self.close) if sys.platform == 'darwin': self.dockMenu = QMenu(self) self.dockMenu.addAction(_('New &Workbench'), self.newWorkbench) self.dockMenu.addAction(_('&New Repository...'), self.newRepository) self.dockMenu.addAction(_('Clon&e Repository...'), self.cloneRepository) self.dockMenu.addAction(_('&Open Repository...'), self.openRepository) qt_mac_set_dock_menu(self.dockMenu) # On Mac OS X, we do not want icons on menus qt_mac_set_menubar_icons(False) # Create the actions that will be displayed on the context menu self.createActions() self.lastClosedRepoRootList = [] self.progressDialog.close() self.progressDialog = None self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth: dlgmeth(self), parent=self) def setupUi(self): desktopgeom = qApp.desktop().availableGeometry() self.resize(desktopgeom.size() * 0.8) self.setWindowIcon(qtlib.geticon('hg-log')) self.repoTabsWidget = tw = QTabWidget() # FIXME setTabBar() is protected method tw.setTabBar(ThgTabBar()) tw.setDocumentMode(True) tw.setTabsClosable(True) tw.setMovable(True) tw.tabBar().hide() tw.tabBar().setContextMenuPolicy(Qt.CustomContextMenu) tw.tabBar().customContextMenuRequested.connect( self.tabBarContextMenuRequest) tw.lastClickedTab = -1 # No tab clicked yet sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(1) sp.setHeightForWidth(tw.sizePolicy().hasHeightForWidth()) tw.setSizePolicy(sp) tw.tabCloseRequested.connect(self.repoTabCloseRequested) tw.currentChanged.connect(self.repoTabChanged) self.setCentralWidget(tw) self.statusbar = cmdui.ThgStatusBar(self) self.setStatusBar(self.statusbar) def _setupActions(self): """Setup actions, menus and toolbars""" self.menubar = QMenuBar(self) self.setMenuBar(self.menubar) self.menuFile = self.menubar.addMenu(_("&File")) self.menuView = self.menubar.addMenu(_("&View")) self.menuRepository = self.menubar.addMenu(_("&Repository")) self.menuHelp = self.menubar.addMenu(_("&Help")) self.edittbar = QToolBar(_("&Edit Toolbar"), objectName='edittbar') self.addToolBar(self.edittbar) self.docktbar = QToolBar(_("&Dock Toolbar"), objectName='docktbar') self.addToolBar(self.docktbar) self.tasktbar = QToolBar(_('&Task Toolbar'), objectName='taskbar') self.addToolBar(self.tasktbar) self.customtbar = QToolBar(_('&Custom Toolbar'), objectName='custombar') self.addToolBar(self.customtbar) self.synctbar = QToolBar(_('S&ync Toolbar'), objectName='synctbar') self.addToolBar(self.synctbar) # availability map of actions; applied by updateMenu() self._actionavails = {'repoopen': []} self._actionvisibles = {'repoopen': []} modifiedkeysequence = qtlib.modifiedkeysequence newaction = self._addNewAction newseparator = self._addNewSeparator newaction(_("New &Workbench"), self.newWorkbench, shortcut='Shift+Ctrl+W', menu='file', icon='hg-log') newseparator(menu='file') newaction(_("&New Repository..."), self.newRepository, shortcut='New', menu='file', icon='hg-init') newaction(_("Clon&e Repository..."), self.cloneRepository, shortcut=modifiedkeysequence('New', modifier='Shift'), menu='file', icon='hg-clone') newseparator(menu='file') newaction(_("&Open Repository..."), self.openRepository, shortcut='Open', menu='file') newaction(_("&Close Repository"), self.closeCurrentRepoTab, shortcut='Close', enabled='repoopen', menu='file') newseparator(menu='file') newaction(_('&Settings'), self.editSettings, icon='settings_user', shortcut='Preferences', menu='file') newseparator(menu='file') newaction(_("E&xit"), self.close, shortcut='Quit', menu='file') a = self.reporegistry.toggleViewAction() a.setText(_('Sh&ow Repository Registry')) a.setShortcut('Ctrl+Shift+O') a.setIcon(qtlib.geticon('thg-reporegistry')) self.docktbar.addAction(a) self.menuView.addAction(a) a = self.mqpatches.toggleViewAction() a.setText(_('Show &Patch Queue')) a.setIcon(qtlib.geticon('thg-mq')) self.docktbar.addAction(a) self.menuView.addAction(a) a = self.log.toggleViewAction() a.setText(_('Show Output &Log')) a.setShortcut('Ctrl+L') a.setIcon(qtlib.geticon('thg-console')) self.docktbar.addAction(a) self.menuView.addAction(a) newseparator(menu='view') menu = self.menuView.addMenu(_('R&epository Registry Options')) menu.addActions(self.reporegistry.settingActions()) newseparator(menu='view') newaction(_("C&hoose Log Columns..."), self.setHistoryColumns, menu='view') self.actionSaveRepos = \ newaction(_("Save Open Repositories on E&xit"), checkable=True, menu='view') newseparator(menu='view') self.actionGroupTaskView = QActionGroup(self) self.actionGroupTaskView.triggered.connect(self.onSwitchRepoTaskTab) def addtaskview(icon, label, name, shortcut=None): a = newaction(label, icon=None, checkable=True, data=name, enabled='repoopen', menu='view') a.setIcon(qtlib.geticon(icon)) if shortcut: a.setShortcut(shortcut) self.actionGroupTaskView.addAction(a) self.tasktbar.addAction(a) return a # note that 'grep' and 'search' are equivalent taskdefs = { 'commit': ('hg-commit', _('&Commit')), 'pbranch': ('branch', _('&Patch Branch')), 'log': ('hg-log', _("Revision &Details")), 'manifest': ('hg-annotate', _('&Manifest')), 'grep': ('hg-grep', _('&Search')), 'sync': ('thg-sync', _('S&ynchronize')), } tasklist = self.ui.configlist( 'tortoisehg', 'workbench.task-toolbar', []) if tasklist == []: tasklist = ['log', 'commit', 'manifest', 'grep', 'pbranch', '|', 'sync'] self.actionSelectTaskPbranch = None for i, taskname in enumerate(tasklist): taskname = taskname.strip() taskinfo = taskdefs.get(taskname, None) if taskinfo is None: newseparator(toolbar='task') continue tbar = addtaskview(taskinfo[0], taskinfo[1], taskname, "Alt+%d" % (i + 1)) if taskname == 'pbranch': self.actionSelectTaskPbranch = tbar newseparator(menu='view') newaction(_("&Refresh"), self.refresh, icon='view-refresh', shortcut='Refresh', enabled='repoopen', menu='view', toolbar='edit', tooltip=_('Refresh current repository')) newaction(_("Refresh &Task Tab"), self._repofwd('reloadTaskTab'), enabled='repoopen', shortcut=modifiedkeysequence('Refresh', modifier='Shift'), tooltip=_('Refresh only the current task tab'), menu='view') newaction(_("Load &All Revisions"), self.loadall, enabled='repoopen', menu='view', shortcut='Shift+Ctrl+A', tooltip=_('Load all revisions into graph')) newseparator(toolbar='edit') newaction(_("Go to current revision"), self._repofwd('gotoParent'), icon='go-home', tooltip=_('Go to current revision'), enabled='repoopen', toolbar='edit', shortcut='Ctrl+.') newaction(_("&Goto Revision..."), self._gotorev, icon='go-to-rev', shortcut='Ctrl+/', enabled='repoopen', tooltip=_('Go to a specific revision'), menu='view', toolbar='edit') menuSync = self.menuRepository.addMenu(_('S&ynchronize')) newseparator(menu='repository') newaction(_("Start &Web Server"), self.serve, menu='repository') newseparator(menu='repository') newaction(_("&Shelve..."), self._repofwd('shelve'), icon='shelve', enabled='repoopen', menu='repository') newaction(_("&Import..."), self._repofwd('thgimport'), icon='hg-import', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("&Verify"), self._repofwd('verify'), enabled='repoopen', menu='repository') newaction(_("Re&cover"), self._repofwd('recover'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("&Resolve..."), self._repofwd('resolve'), icon='hg-merge', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Rollback/&Undo..."), self._repofwd('rollback'), shortcut='Ctrl+u', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("&Purge..."), self._repofwd('purge'), enabled='repoopen', icon='hg-purge', menu='repository') newseparator(menu='repository') newaction(_("&Bisect..."), self._repofwd('bisect'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("E&xplore"), self.explore, shortcut='Shift+Ctrl+X', icon='system-file-manager', enabled='repoopen', menu='repository') newaction(_("&Terminal"), self.terminal, shortcut='Shift+Ctrl+T', icon='utilities-terminal', enabled='repoopen', menu='repository') newaction(_("&Help"), self.onHelp, menu='help', icon='help-browser') newaction(_("E&xplorer Help"), self.onHelpExplorer, menu='help') visiblereadme = 'repoopen' if self.ui.config('tortoisehg', 'readme', None): visiblereadme = True newaction(_("&Readme"), self.onReadme, menu='help', icon='help-readme', visible=visiblereadme, shortcut='Ctrl+F1') newseparator(menu='help') newaction(_("About &Qt"), QApplication.aboutQt, menu='help') newaction(_("&About TortoiseHg"), self.onAbout, menu='help', icon='thg-logo') self.actionBack = \ newaction(_("Back"), self._repofwd('back'), icon='go-previous', enabled=False, toolbar='edit') self.actionForward = \ newaction(_("Forward"), self._repofwd('forward'), icon='go-next', enabled=False, toolbar='edit') newseparator(toolbar='edit', menu='View') self.filtertbaction = \ newaction(_('&Filter Toolbar'), self._repotogglefwd('toggleFilterBar'), icon='view-filter', shortcut='Ctrl+S', enabled='repoopen', toolbar='edit', menu='View', checkable=True, tooltip=_('Filter graph with revision sets or branches')) menu = QMenu(_('&Workbench Toolbars'), self) menu.addAction(self.edittbar.toggleViewAction()) menu.addAction(self.docktbar.toggleViewAction()) menu.addAction(self.tasktbar.toggleViewAction()) menu.addAction(self.synctbar.toggleViewAction()) menu.addAction(self.customtbar.toggleViewAction()) self.menuView.addMenu(menu) newaction(_('&Incoming'), data='incoming', icon='hg-incoming', enabled='repoopen', toolbar='sync', shortcut='Ctrl+Shift+,') newaction(_('&Pull'), data='pull', icon='hg-pull', enabled='repoopen', toolbar='sync') newaction(_('&Outgoing'), data='outgoing', icon='hg-outgoing', enabled='repoopen', toolbar='sync', shortcut='Ctrl+Shift+.') newaction(_('P&ush'), data='push', icon='hg-push', enabled='repoopen', toolbar='sync') menuSync.addActions(self.synctbar.actions()) self.urlCombo = QComboBox(self) self.urlCombo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.urlCombo.currentIndexChanged.connect(self._updateSyncUrlToolTip) self.urlComboAction = self.synctbar.addWidget(self.urlCombo) # hide it because workbench could be started without open repo self.urlComboAction.setVisible(False) self.synctbar.actionTriggered.connect(self._runSyncAction) self.updateMenu() def _setupUrlCombo(self, repo): """repository has been switched, fill urlCombo with URLs""" pathdict = dict((hglib.tounicode(alias), hglib.tounicode(path)) for alias, path in repo.ui.configitems('paths')) aliases = pathdict.keys() combo_setting = repo.ui.config('tortoisehg', 'workbench.target-combo', 'auto') self.urlComboAction.setVisible(len(aliases) > 1 or combo_setting == 'always') # 1. Sort the list if aliases aliases.sort() # 2. Place the default alias at the top of the list if 'default' in aliases: aliases.remove('default') aliases.insert(0, 'default') # 3. Make a list of paths that have a 'push path' # note that the default path will be first (if it has a push path), # followed by the other paths that have a push path, alphabetically haspushaliases = [alias for alias in aliases if alias + '-push' in aliases] # 4. Place the "-push" paths next to their "pull paths" regularaliases = [] for a in aliases[:]: if a.endswith('-push'): if a[:-len('-push')] in haspushaliases: continue regularaliases.append(a) if a in haspushaliases: regularaliases.append(a + '-push') # 5. Create the list of 'combined aliases' combinedaliases = [(a, a + '-push') for a in haspushaliases] # 6. Put the combined aliases first, followed by the regular aliases aliases = combinedaliases + regularaliases # 7. Ensure the first path is a default path (either a # combined "default | default-push" path or a regular default path) if not 'default-push' in aliases and 'default' in aliases: aliases.remove('default') aliases.insert(0, 'default') self.urlCombo.blockSignals(True) self.urlCombo.clear() for n, a in enumerate(aliases): # text, (pull-alias, push-alias) if isinstance(a, tuple): itemtext = u'\u2193 %s | %s \u2191' % a itemdata = tuple(pathdict[alias] for alias in a) tooltip = _('pull: %s\npush: %s') % itemdata else: itemtext = a itemdata = (pathdict[a], pathdict[a]) tooltip = pathdict[a] self.urlCombo.addItem(itemtext, itemdata) self.urlCombo.setItemData(n, tooltip, Qt.ToolTipRole) self.urlCombo.blockSignals(False) self._updateSyncUrlToolTip(self.urlCombo.currentIndex()) @pyqtSlot(unicode) def _setupUrlComboIfCurrent(self, root): w = self.repoTabsWidget.currentWidget() if w.repoRootPath() == root: self._setupUrlCombo(w.repo) def _syncUrlFor(self, op): """Current URL for the given sync operation""" urlindex = self.urlCombo.currentIndex() if urlindex < 0: return opindex = {'incoming': 0, 'pull': 0, 'outgoing': 1, 'push': 1}[op] return self.urlCombo.itemData(urlindex).toPyObject()[opindex] @pyqtSlot(int) def _updateSyncUrlToolTip(self, index): self._updateUrlComboToolTip(index) self._updateSyncActionToolTip(index) def _updateUrlComboToolTip(self, index): if not self.urlCombo.count(): tooltip = _('There are no configured sync paths.\n' 'Open the Synchronize tab to configure them.') else: tooltip = self.urlCombo.itemData(index, Qt.ToolTipRole).toString() self.urlCombo.setToolTip(tooltip) def _updateSyncActionToolTip(self, index): if index < 0: tooltips = { 'incoming': _('Check for incoming changes'), 'pull': _('Pull incoming changes'), 'outgoing': _('Detect outgoing changes'), 'push': _('Push outgoing changes'), } else: pullurl, pushurl = self.urlCombo.itemData(index).toPyObject() tooltips = { 'incoming': _('Check for incoming changes from\n%s') % pullurl, 'pull': _('Pull incoming changes from\n%s') % pullurl, 'outgoing': _('Detect outgoing changes to\n%s') % pushurl, 'push': _('Push outgoing changes to\n%s') % pushurl, } for a in self.synctbar.actions(): op = str(a.data().toString()) if op in tooltips: a.setToolTip(tooltips[op]) def _setupCustomTools(self, ui): tools, toollist = hglib.tortoisehgtools(ui, selectedlocation='workbench.custom-toolbar') # Clear the existing "custom" toolbar self.customtbar.clear() # and repopulate it again with the tool configuration # for the current repository if not tools: return for name in toollist: if name == '|': self._addNewSeparator(toolbar='custom') continue info = tools.get(name, None) if info is None: continue command = info.get('command', None) if not command: continue showoutput = info.get('showoutput', False) workingdir = info.get('workingdir', '') label = info.get('label', name) tooltip = info.get('tooltip', _("Execute custom tool '%s'") % label) icon = info.get('icon', 'tools-spanner-hammer') self._addNewAction(label, self._repofwd('runCustomCommand', [command, showoutput, workingdir]), icon=icon, tooltip=tooltip, enabled=True, toolbar='custom') def _addNewAction(self, text, slot=None, icon=None, shortcut=None, checkable=False, tooltip=None, data=None, enabled=None, visible=None, menu=None, toolbar=None): """Create new action and register it :slot: function called if action triggered or toggled. :checkable: checkable action. slot will be called on toggled. :data: optional data stored on QAction. :enabled: bool or group name to enable/disable action. :visible: bool or group name to show/hide action. :shortcut: QKeySequence, key sequence or name of standard key. :menu: name of menu to add this action. :toolbar: name of toolbar to add this action. """ action = QAction(text, self, checkable=checkable) if slot: if checkable: action.toggled.connect(slot) else: action.triggered.connect(slot) if icon: action.setIcon(qtlib.geticon(icon)) if shortcut: keyseq = qtlib.keysequence(shortcut) if isinstance(keyseq, QKeySequence.StandardKey): action.setShortcuts(keyseq) else: action.setShortcut(keyseq) if tooltip: action.setToolTip(tooltip) if data is not None: action.setData(data) if isinstance(enabled, bool): action.setEnabled(enabled) elif enabled: self._actionavails[enabled].append(action) if isinstance(visible, bool): action.setVisible(visible) elif visible: self._actionvisibles[visible].append(action) if menu: getattr(self, 'menu%s' % menu.title()).addAction(action) if toolbar: getattr(self, '%stbar' % toolbar).addAction(action) return action def _addNewSeparator(self, menu=None, toolbar=None): """Insert a separator action; returns nothing""" if menu: getattr(self, 'menu%s' % menu.title()).addSeparator() if toolbar: getattr(self, '%stbar' % toolbar).addSeparator() def _action_defs(self): a = [("closetab", _("Close tab"), '', _("Close tab"), self.closeLastClickedTab), ("closeothertabs", _("Close other tabs"), '', _("Close other tabs"), self.closeNotLastClickedTabs), ("reopenlastclosed", _("Undo close tab"), '', _("Reopen last closed tab"), self.reopenLastClosedTabs), ("reopenlastclosedgroup", _("Undo close other tabs"), '', _("Reopen last closed tab group"), self.reopenLastClosedTabs), ] return a def createActions(self): self._actions = {} for name, desc, icon, tip, cb in self._action_defs(): self._actions[name] = QAction(desc, self) QTimer.singleShot(0, self.configureActions) def configureActions(self): for name, desc, icon, tip, cb in self._action_defs(): act = self._actions[name] if icon: act.setIcon(qtlib.geticon(icon)) if tip: act.setStatusTip(tip) if cb: act.triggered.connect(cb) self.addAction(act) def createPopupMenu(self): """Create new popup menu for toolbars and dock widgets""" menu = super(Workbench, self).createPopupMenu() assert menu # should have toolbar/dock menu menu.addSeparator() menu.addAction(_('Custom Toolbar &Settings'), self._editCustomToolsSettings) return menu @pyqtSlot(QPoint) def tabBarContextMenuRequest(self, point): # Activate the clicked tab clickedwidget = qApp.widgetAt(self.repoTabsWidget.mapToGlobal(point)) if not clickedwidget or \ not isinstance(clickedwidget, ThgTabBar): return self.repoTabsWidget.lastClickedTab = -1 clickedtabindex = clickedwidget.tabAt(point) if clickedtabindex > -1: self.repoTabsWidget.lastClickedTab = clickedtabindex else: self.repoTabsWidget.lastClickedTab = self.repoTabsWidget.currentIndex() actionlist = ['closetab', 'closeothertabs'] existingClosedRepoList = [] for reporoot in self.lastClosedRepoRootList: if os.path.isdir(reporoot): existingClosedRepoList.append(reporoot) self.lastClosedRepoRootList = existingClosedRepoList if len(self.lastClosedRepoRootList) > 1: actionlist += ['', 'reopenlastclosedgroup'] elif len(self.lastClosedRepoRootList) > 0: actionlist += ['', 'reopenlastclosed'] contextmenu = QMenu(self) for act in actionlist: if act: contextmenu.addAction(self._actions[act]) else: contextmenu.addSeparator() if actionlist: contextmenu.setAttribute(Qt.WA_DeleteOnClose) contextmenu.popup(self.repoTabsWidget.mapToGlobal(point)) @pyqtSlot() def closeLastClickedTab(self): if self.repoTabsWidget.lastClickedTab > -1: self.repoTabCloseRequested(self.repoTabsWidget.lastClickedTab) def _closeOtherTabs(self, tabIndex): if tabIndex > -1: tb = self.repoTabsWidget.tabBar() tb.setCurrentIndex(tabIndex) closedRepoRootList = [] for idx in range(tb.count()-1, -1, -1): if idx != tabIndex: self.repoTabCloseRequested(idx) # repoTabCloseRequested updates self.lastClosedRepoRootList closedRepoRootList += self.lastClosedRepoRootList self.lastClosedRepoRootList = closedRepoRootList @pyqtSlot() def closeNotLastClickedTabs(self): self._closeOtherTabs(self.repoTabsWidget.lastClickedTab) def onSwitchRepoTaskTab(self, action): rw = self.repoTabsWidget.currentWidget() if rw: rw.switchToNamedTaskTab(str(action.data().toString())) @pyqtSlot(QString, bool) def openRepo(self, root, reuse, bundle=None): """Open tab of the specified repo [unicode]""" root = unicode(root) if root and not root.startswith('ssh://'): if reuse: for rw in self._findRepoWidget(root): self.repoTabsWidget.setCurrentWidget(rw) return try: repoagent = self._repomanager.openRepoAgent(root) self.addRepoTab(repoagent, bundle) except RepoError, e: qtlib.WarningMsgBox(_('Failed to open repository'), hglib.tounicode(str(e)), parent=self) @pyqtSlot(QString) def closeRepo(self, root): """Close tabs of the specified repo [unicode]""" for rw in list(self._findRepoWidget(unicode(root))): self.repoTabCloseRequested(self.repoTabsWidget.indexOf(rw)) @pyqtSlot(QString) def openLinkedRepo(self, path): uri = unicode(path).split('?', 1) path = uri[0] rev = None if len(uri) > 1: rev = hglib.fromunicode(uri[1]) self.showRepo(path) rw = self.repoTabsWidget.currentWidget() if rw and rw.repoRootPath() == os.path.normpath(path): if rev: rw.goto(rev) else: # assumes that the request comes from commit widget; in this # case, the user is going to commit changes to this repo. rw.taskTabsWidget.setCurrentIndex(rw.commitTabIndex) @pyqtSlot(QString) def showRepo(self, root): """Activate the repo tab or open it if not available [unicode]""" self.openRepo(root, True) @pyqtSlot(unicode, QString) def setRevsetFilter(self, path, filter): for i in xrange(self.repoTabsWidget.count()): w = self.repoTabsWidget.widget(i) if w.repoRootPath() == path: w.setFilter(filter) return def find_root(self, url): p = hglib.fromunicode(url.toLocalFile()) return paths.find_root(p) def dragEnterEvent(self, event): d = event.mimeData() for u in d.urls(): root = self.find_root(u) if root: event.setDropAction(Qt.LinkAction) event.accept() break def dropEvent(self, event): accept = False d = event.mimeData() for u in d.urls(): root = self.find_root(u) if root: self.showRepo(hglib.tounicode(root)) accept = True if accept: event.setDropAction(Qt.LinkAction) event.accept() def updateMenu(self): """Enable actions when repoTabs are opened or closed or changed""" # Update actions affected by repo open/close someRepoOpen = self.repoTabsWidget.count() > 0 for action in self._actionavails['repoopen']: action.setEnabled(someRepoOpen) for action in self._actionvisibles['repoopen']: action.setVisible(someRepoOpen) # Update actions affected by repo open/close/change self.updateTaskViewMenu() self.updateToolBarActions() tw = self.repoTabsWidget if ((tw.count() == 0) or ((tw.count() == 1) and not self.ui.configbool('tortoisehg', 'forcerepotab', False))): tw.tabBar().hide() else: tw.tabBar().show() self._updateWindowTitle() def _updateWindowTitle(self): tw = self.repoTabsWidget w = tw.currentWidget() if tw.count() == 0: self.setWindowTitle(_('TortoiseHg Workbench')) elif w.repo.ui.configbool('tortoisehg', 'fullpath'): self.setWindowTitle(_('%s - TortoiseHg Workbench - %s') % (w.title(), w.repoRootPath())) else: self.setWindowTitle(_('%s - TortoiseHg Workbench') % w.title()) def updateToolBarActions(self): w = self.repoTabsWidget.currentWidget() if w: self.filtertbaction.setChecked(w.filterBarVisible()) def updateTaskViewMenu(self): 'Update task tab menu for current repository' if self.repoTabsWidget.count() == 0: for a in self.actionGroupTaskView.actions(): a.setChecked(False) if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible(False) else: repoWidget = self.repoTabsWidget.currentWidget() exts = repoWidget.repo.extensions() if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible('pbranch' in exts) taskIndex = repoWidget.taskTabsWidget.currentIndex() for name, idx in repoWidget.namedTabs.iteritems(): if idx == taskIndex: break for action in self.actionGroupTaskView.actions(): if str(action.data().toString()) == name: action.setChecked(True) @pyqtSlot() def updateHistoryActions(self): 'Update back / forward actions' rw = self.repoTabsWidget.currentWidget() if not rw: return self.actionBack.setEnabled(rw.canGoBack()) self.actionForward.setEnabled(rw.canGoForward()) @pyqtSlot(int) def repoTabCloseRequested(self, index): tw = self.repoTabsWidget if 0 <= index < tw.count(): w = tw.widget(index) reporoot = w.repoRootPath() if w.closeRepoWidget(): tw.removeTab(index) w.deleteLater() self._repomanager.releaseRepoAgent(reporoot) self.updateMenu() self.lastClosedRepoRootList = [reporoot] @pyqtSlot() def reopenLastClosedTabs(self): for n, reporoot in enumerate(self.lastClosedRepoRootList): self.progress(_('Reopening tabs'), n, _('Reopening repository %s') % reporoot, '', len(self.lastClosedRepoRootList)) if os.path.isdir(reporoot): self.showRepo(reporoot) self.lastClosedRepoRootList = [] self.progress(_('Reopening tabs'), None, '', '', None) @pyqtSlot() def repoTabChanged(self): w = self.repoTabsWidget.currentWidget() if w: self.updateHistoryActions() self.updateMenu() self.log.setCurrentRepoRoot(w.repoRootPath()) repoagent = self._repomanager.repoAgent(w.repoRootPath()) self.mqpatches.setRepoAgent(repoagent) self.reporegistry.setActiveTabRepo(w.repoRootPath()) self._setupCustomTools(w.repo.ui) self._setupUrlCombo(w.repo) else: self.log.setCurrentRepoRoot(None) self.mqpatches.setRepoAgent(None) self.reporegistry.setActiveTabRepo('') @pyqtSlot(QWidget) def _updateRepoTabTitle(self, rw): index = self.repoTabsWidget.indexOf(rw) self.repoTabsWidget.setTabText(index, rw.title()) if index == self.repoTabsWidget.currentIndex(): self._updateWindowTitle() @pyqtSlot(QWidget) def _updateRepoTabIcon(self, rw): index = self.repoTabsWidget.indexOf(rw) self.repoTabsWidget.setTabIcon(index, rw.busyIcon()) def addRepoTab(self, repoagent, bundle): '''opens the given repo in a new tab''' rw = RepoWidget(repoagent, self, bundle=bundle) rw.showMessageSignal.connect(self.showMessage) rw.progress.connect(lambda tp, p, i, u, tl: self.statusbar.progress(tp, p, i, u, tl, rw.repo.root)) rw.output.connect(self._appendRepoWidgetOutput) rw.makeLogVisible.connect(self.log.setShown) rw.revisionSelected.connect(self.updateHistoryActions) rw.repoLinkClicked.connect(self.openLinkedRepo) rw.taskTabsWidget.currentChanged.connect(self.updateTaskViewMenu) rw.toolbarVisibilityChanged.connect(self.updateToolBarActions) tw = self.repoTabsWidget # We can open new tabs next to the current one or next to the last tab openTabAfterCurrent = self.ui.configbool('tortoisehg', 'opentabsaftercurrent', True) if openTabAfterCurrent: index = self.repoTabsWidget.insertTab( tw.currentIndex()+1, rw, rw.title()) else: index = self.repoTabsWidget.addTab(rw, rw.title()) tw.setTabToolTip(index, repoagent.rootPath()) tw.setCurrentIndex(index) # PyQt 4.6 cannot find compatible signal by new-style connection QObject.connect(rw, SIGNAL('titleChanged(QString)'), self._repoTabTitleChangedMapper, SLOT('map()')) self._repoTabTitleChangedMapper.setMapping(rw, rw) QObject.connect(rw, SIGNAL('busyIconChanged()'), self._repoTabBusyIconChangedMapper, SLOT('map()')) self._repoTabBusyIconChangedMapper.setMapping(rw, rw) self.reporegistry.addRepo(repoagent.rootPath()) self.updateMenu() return rw #@pyqtSlot(QString, QString) def _appendRepoWidgetOutput(self, msg, label): rw = self.sender() assert isinstance(rw, RepoWidget) self.log.appendLog(msg, label, rw.repoRootPath()) def showMessage(self, msg): self.statusbar.showMessage(msg) @pyqtSlot(QString, object, QString, QString, object) def progress(self, topic, pos, item, unit, total=100, root=None): if self.progressDialog: if pos is None: self.progressDialog.close() return if total is None: total = 100 pos = round(pos) total = round(total) self.progressDialog.setWindowTitle('TortoiseHg - %s' % topic) self.progressDialog.setLabelText('%s (%d / %d)' % (item, pos, total)) self.progressDialog.setMaximum(total) self.progressDialog.show() self.progressDialog.setValue(pos) else: self.statusbar.progress(topic, pos, item, unit, total, root) def setHistoryColumns(self, *args): """Display the column selection dialog""" w = self.repoTabsWidget.currentWidget() dlg = ColumnSelectDialog('workbench', _('Workbench'), w and w.repoview.model() or None) if dlg.exec_() == QDialog.Accepted: if w: w.repoview.model().updateColumns() w.repoview.resizeColumns() def _repotogglefwd(self, name): """Return function to forward action to the current repo tab""" def forwarder(checked): w = self.repoTabsWidget.currentWidget() if w: getattr(w, name)(checked) return forwarder def _repofwd(self, name, params=[], namedparams={}): """Return function to forward action to the current repo tab""" def forwarder(): w = self.repoTabsWidget.currentWidget() if w: getattr(w, name)(*params, **namedparams) return forwarder @pyqtSlot() def refresh(self): w = self.repoTabsWidget.currentWidget() if w: # check unnoticed changes to emit corresponding signals repoagent = self._repomanager.repoAgent(w.repoRootPath()) repoagent.pollStatus() # TODO if all objects are responsive to repository signals, some # of the following actions are not necessary getattr(w, 'reload')() self._setupUrlCombo(w.repo) if not self.mqpatches.isHidden(): self.mqpatches.reload() # no @pyqtSlot(QAction) to avoid wrong self.sender() at # _appendRepoWidgetOutput # during QMessageBox.exec at SyncWidget.pushclicked. See #3320. def _runSyncAction(self, action): w = self.repoTabsWidget.currentWidget() if w: op = str(action.data().toString()) w.setSyncUrl(self._syncUrlFor(op) or '') getattr(w, op)() def serve(self): self._dialogs.open(Workbench._createServeDialog) def _createServeDialog(self): w = self.repoTabsWidget.currentWidget() if w: return serve.run(w.repo.ui, root=w.repo.root) else: return serve.run(self.ui) def loadall(self): w = self.repoTabsWidget.currentWidget() if w: w.repoview.model().loadall() def _gotorev(self): rev, ok = qtlib.getTextInput(self, _("Goto revision"), _("Enter revision identifier")) if ok: self.gotorev(rev) @pyqtSlot(unicode) def gotorev(self, rev): w = self.repoTabsWidget.currentWidget() if w: w.repoview.goto(rev) def newWorkbench(self): from tortoisehg.hgqt.run import portable_start_fork portable_start_fork(['--new']) def newRepository(self): """ Run init dialog """ from tortoisehg.hgqt.hginit import InitDialog repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: path = os.path.dirname(repoWidget.repo.root) else: path = os.getcwd() dlg = InitDialog([path], parent=self) dlg.finished.connect(dlg.deleteLater) if dlg.exec_(): path = dlg.getPath() self.openRepo(hglib.tounicode(path), False) def cloneRepository(self): """ Run clone dialog """ # it might be better to reuse existing CloneDialog dlg = self._dialogs.openNew(Workbench._createCloneDialog) repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: uroot = repoWidget.repoRootPath() dlg.setSource(uroot) dlg.setDestination(uroot + '-clone') def _createCloneDialog(self): from tortoisehg.hgqt.clone import CloneDialog dlg = CloneDialog(parent=self) dlg.clonedRepository.connect(self.showRepo) return dlg def openRepository(self): """ Open repo from File menu """ caption = _('Select repository directory to open') repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: cwd = os.path.dirname(repoWidget.repo.root) else: cwd = os.getcwd() cwd = hglib.tounicode(cwd) FD = QFileDialog path = FD.getExistingDirectory(self, caption, cwd, FD.ShowDirsOnly | FD.ReadOnly) self.openRepo(path, False) def _findRepoWidget(self, root): """Iterates RepoWidget for the specified root""" def normpathandcase(path): return os.path.normcase(os.path.normpath(path)) normroot = normpathandcase(root) tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) if normpathandcase(rw.repoRootPath()) == normroot: yield rw def onAbout(self, *args): """ Display about dialog """ from tortoisehg.hgqt.about import AboutDialog ad = AboutDialog(self) ad.finished.connect(ad.deleteLater) ad.exec_() def onHelp(self, *args): """ Display online help """ qtlib.openhelpcontents('workbench.html') def onHelpExplorer(self, *args): """ Display online help for shell extension """ qtlib.openhelpcontents('explorer.html') def onReadme(self, *args): """Display the README file or URL for the current repo, or the global README if no repo is open""" readme = None def getCurrentReadme(repo): """ Get the README file that is configured for the current repo. README files can be set in 3 ways, which are checked in the following order of decreasing priority: - From the tortoisehg.readme key on the current repo's configuration file - An existing "README" file found on the repository root * Valid README files are those called README and whose extension is one of the following: ['', '.txt', '.html', '.pdf', '.doc', '.docx', '.ppt', '.pptx', '.markdown', '.textile', '.rdoc', '.org', '.creole', '.mediawiki','.rst', '.asciidoc', '.pod'] * Note that the match is CASE INSENSITIVE on ALL OSs. - From the tortoisehg.readme key on the user's global configuration file """ readme = None if repo: # Try to get the README configured for the repo of the current tab readmeglobal = self.ui.config('tortoisehg', 'readme', None) if readmeglobal: # Note that repo.ui.config() falls back to the self.ui.config() # if the key is not set on the current repo's configuration file readme = repo.ui.config('tortoisehg', 'readme', None) if readmeglobal != readme: # The readme is set on the current repo configuration file return readme # Otherwise try to see if there is a file at the root of the # repository that matches any of the valid README file names # (in a non case-sensitive way) # Note that we try to match the valid README names in order validreadmes = ['readme.txt', 'read.me', 'readme.html', 'readme.pdf', 'readme.doc', 'readme.docx', 'readme.ppt', 'readme.pptx', 'readme.md', 'readme.markdown', 'readme.mkdn', 'readme.rst', 'readme.textile', 'readme.rdoc', 'readme.asciidoc', 'readme.org', 'readme.creole', 'readme.mediawiki', 'readme.pod', 'readme'] readmefiles = [filename for filename in os.listdir(repo.root) if filename.lower().startswith('read')] for validname in validreadmes: for filename in readmefiles: if filename.lower() == validname: return repo.wjoin(filename) # Otherwise try use the global setting (or None if readme is just # not configured) return readmeglobal w = self.repoTabsWidget.currentWidget() if w: # Try to get the help doc from the current repo tap readme = getCurrentReadme(w.repo) if readme: qtlib.openlocalurl(os.path.expandvars(os.path.expandvars(readme))) else: qtlib.WarningMsgBox(_("README not configured"), _("A README file is not configured for the current repository.

    " "To configure a README file for a repository, " "open the repository settings file, add a 'readme' " "key to the 'tortoisehg' section, and set it " "to the filename or URL of your repository's README file.")) def storeSettings(self): s = QSettings() wb = "Workbench/" s.setValue(wb + 'geometry', self.saveGeometry()) s.setValue(wb + 'windowState', self.saveState()) s.setValue(wb + 'saveRepos', self.actionSaveRepos.isChecked()) repostosave = [] lastactiverepo = '' if self.actionSaveRepos.isChecked(): tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) repostosave.append(rw.repoRootPath()) cw = tw.currentWidget() if cw is not None: lastactiverepo = cw.repoRootPath() s.setValue(wb + 'lastactiverepo', lastactiverepo) s.setValue(wb + 'openrepos', (',').join(repostosave)) def restoreSettings(self): s = QSettings() wb = "Workbench/" self.restoreGeometry(s.value(wb + 'geometry').toByteArray()) self.restoreState(s.value(wb + 'windowState').toByteArray()) save = s.value(wb + 'saveRepos').toBool() self.actionSaveRepos.setChecked(save) # Reload the all the repos that were open on the last session. # This may be a lengthy operation, which happens before the Workbench # GUI is open. We use a progress dialog to let the user know that the # workbench is being loaded openreposvalue = unicode(s.value(wb + 'openrepos').toString()) if openreposvalue: openrepos = openreposvalue.split(',') else: openrepos = [] for n, upath in enumerate(openrepos): self.progress(_('Reopening tabs'), n, _('Reopening repository %s') % upath, '', len(openrepos)) QCoreApplication.processEvents() self.openRepo(upath, False) QCoreApplication.processEvents() self.progress(_('Reopening tabs'), None, '', '', None) # Activate the tab that was last active on the last session (if any) # Note that if a "root" has been passed to the "thg" command, # this will have no effect lastactiverepo = s.value(wb + 'lastactiverepo').toString() if lastactiverepo: self.openRepo(lastactiverepo, True) # Clear the lastactiverepo and the openrepos list once the workbench state # has been reload, so that opening additional workbench windows does not # reopen these repos again s.setValue(wb + 'openrepos', '') s.setValue(wb + 'lastactiverepo', '') def goto(self, root, rev): for rw in self._findRepoWidget(hglib.tounicode(root)): rw.goto(rev) def closeEvent(self, event): if not self.closeRepoTabs(): event.ignore() else: self.storeSettings() self.reporegistry.close() # mimic QDialog exit self.finished.emit(0) def closeRepoTabs(self): '''returns False if close should be aborted''' tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) if not rw.closeRepoWidget(): tw.setCurrentWidget(rw) return False return True @pyqtSlot() def closeCurrentRepoTab(self): """close the current repo tab""" self.repoTabCloseRequested(self.repoTabsWidget.currentIndex()) def explore(self): w = self.repoTabsWidget.currentWidget() if w: qtlib.openlocalurl(w.repo.root) def terminal(self): w = self.repoTabsWidget.currentWidget() if w: qtlib.openshell(w.repo.root, w.repo.displayname, w.repo.ui) @pyqtSlot() def editSettings(self, focus=None): tw = self.repoTabsWidget w = tw.currentWidget() twrepo = (w and w.repo.root or '') sd = SettingsDialog(configrepo=False, focus=focus, parent=self, root=twrepo) sd.exec_() @pyqtSlot() def _editCustomToolsSettings(self): self.editSettings('tools') tortoisehg-2.10/tortoisehg/hgqt/hgignore.py0000644000076400007640000002617212231647662020220 0ustar stevesteve# hgignore.py - TortoiseHg's dialog for editing .hgignore # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import re from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import commands, match, ui, util, error from tortoisehg.hgqt.i18n import _ from tortoisehg.util import shlib, hglib from tortoisehg.hgqt import qtlib, qscilib class HgignoreDialog(QDialog): 'Edit a repository .hgignore file' ignoreFilterUpdated = pyqtSignal() contextmenu = None def __init__(self, repoagent, parent=None, *pats): 'Initialize the Dialog' QDialog.__init__(self, parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self._repoagent = repoagent repo = repoagent.rawRepo() self.pats = pats self.setWindowTitle(_('Ignore filter - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('ignore')) vbox = QVBoxLayout() self.setLayout(vbox) # layer 1 hbox = QHBoxLayout() vbox.addLayout(hbox) recombo = QComboBox() recombo.addItems([_('Glob'), _('Regexp')]) hbox.addWidget(recombo) le = QLineEdit() hbox.addWidget(le, 1) le.returnPressed.connect(self.addEntry) add = QPushButton(_('Add')) add.clicked.connect(self.addEntry) hbox.addWidget(add, 0) # layer 2 hbox = QHBoxLayout() vbox.addLayout(hbox) ignorefiles = [repo.wjoin('.hgignore')] for name, value in repo.ui.configitems('ui'): if name == 'ignore' or name.startswith('ignore.'): ignorefiles.append(util.expandpath(value)) filecombo = QComboBox() hbox.addWidget(filecombo) for f in ignorefiles: filecombo.addItem(hglib.tounicode(f)) filecombo.currentIndexChanged.connect(self.fileselect) self.ignorefile = ignorefiles[0] edit = QPushButton(_('Edit File')) edit.clicked.connect(self.editClicked) hbox.addWidget(edit) hbox.addStretch(1) # layer 3 - main widgets split = QSplitter() vbox.addWidget(split, 1) ignoregb = QGroupBox() ivbox = QVBoxLayout() ignoregb.setLayout(ivbox) lbl = QLabel(_('Ignore Filter')) ivbox.addWidget(lbl) split.addWidget(ignoregb) unknowngb = QGroupBox() uvbox = QVBoxLayout() unknowngb.setLayout(uvbox) lbl = QLabel(_('Untracked Files')) uvbox.addWidget(lbl) split.addWidget(unknowngb) ignorelist = QListWidget() ivbox.addWidget(ignorelist) ignorelist.setSelectionMode(QAbstractItemView.ExtendedSelection) unknownlist = QListWidget() uvbox.addWidget(unknownlist) unknownlist.setSelectionMode(QAbstractItemView.ExtendedSelection) unknownlist.currentTextChanged.connect(self.setGlobFilter) unknownlist.setContextMenuPolicy(Qt.CustomContextMenu) unknownlist.customContextMenuRequested.connect(self.menuRequest) unknownlist.itemDoubleClicked.connect(self.unknownDoubleClicked) lbl = QLabel(_('Backspace or Del to remove row(s)')) ivbox.addWidget(lbl) # layer 4 - dialog buttons BB = QDialogButtonBox bb = QDialogButtonBox(BB.Close) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) vbox.addWidget(bb) self.bb = bb le.setFocus() self.le, self.recombo, self.filecombo = le, recombo, filecombo self.ignorelist, self.unknownlist = ignorelist, unknownlist ignorelist.installEventFilter(self) QTimer.singleShot(0, self.refresh) s = QSettings() self.restoreGeometry(s.value('hgignore/geom').toByteArray()) @property def repo(self): return self._repoagent.rawRepo() def eventFilter(self, obj, event): if obj != self.ignorelist: return False if event.type() != QEvent.KeyPress: return False elif event.key() not in (Qt.Key_Backspace, Qt.Key_Delete): return False if obj.currentRow() < 0: return False for idx in sorted(obj.selectedIndexes(), reverse=True): self.ignorelines.pop(idx.row()) self.writeIgnoreFile() self.refresh() return True def menuRequest(self, point): 'context menu request for unknown list' point = self.unknownlist.viewport().mapToGlobal(point) selected = [self.lclunknowns[i.row()] for i in sorted(self.unknownlist.selectedIndexes())] if len(selected) == 0: return if not self.contextmenu: self.contextmenu = QMenu(self) self.contextmenu.setTitle(_('Add ignore filter...')) else: self.contextmenu.clear() filters = [] if len(selected) == 1: local = selected[0] filters.append([local]) dirname = os.path.dirname(local) while dirname: filters.append([dirname]) dirname = os.path.dirname(dirname) base, ext = os.path.splitext(local) if ext: filters.append(['*'+ext]) filters.append(['**'+ext]) else: filters.append(selected) for f in filters: n = len(f) == 1 and f[0] or _('selected files') a = self.contextmenu.addAction(_('Ignore ') + hglib.tounicode(n)) a._patterns = f a.triggered.connect(self.insertFilters) self.contextmenu.exec_(point) def unknownDoubleClicked(self, item): self.insertFilters([hglib.fromunicode(item.text())]) def insertFilters(self, pats=False, isregexp=False): if pats is False: pats = self.sender()._patterns h = isregexp and 'syntax: regexp' or 'syntax: glob' if h in self.ignorelines: l = self.ignorelines.index(h) for i, line in enumerate(self.ignorelines[l+1:]): if line.startswith('syntax:'): for pat in pats: self.ignorelines.insert(l+i+1, pat) break else: self.ignorelines.extend(pats) else: self.ignorelines.append(h) self.ignorelines.extend(pats) self.writeIgnoreFile() self.refresh() def setGlobFilter(self, qstr): 'user selected an unknown file; prep a glob filter' self.recombo.setCurrentIndex(0) self.le.setText(qstr) def fileselect(self): 'user selected another ignore file' self.ignorefile = hglib.fromunicode(self.filecombo.currentText()) self.refresh() def editClicked(self): if qscilib.fileEditor(self.ignorefile) == QDialog.Accepted: self.refresh() def addEntry(self): newfilter = hglib.fromunicode(self.le.text()).strip() if newfilter == '': return self.le.clear() if self.recombo.currentIndex() == 0: test = 'glob:' + newfilter try: match.match(self.repo.root, '', [], [test]) self.insertFilters([newfilter], False) except util.Abort, inst: qtlib.WarningMsgBox(_('Invalid glob expression'), str(inst), parent=self) return else: test = 'relre:' + newfilter try: match.match(self.repo.root, '', [], [test]) re.compile(test) self.insertFilters([newfilter], True) except (util.Abort, re.error), inst: qtlib.WarningMsgBox(_('Invalid regexp expression'), str(inst), parent=self) return def refresh(self): try: l = open(self.ignorefile, 'rb').readlines() self.doseoln = l[0].endswith('\r\n') except (IOError, ValueError, IndexError): self.doseoln = os.name == 'nt' l = [] self.ignorelines = [line.strip() for line in l] self.ignorelist.clear() uni = hglib.tounicode self.ignorelist.addItems([uni(l) for l in self.ignorelines]) try: self.repo.thginvalidate() self.repo.lfstatus = True wctx = self.repo[None] wctx.status(unknown=True) self.repo.lfstatus = False except (EnvironmentError, error.RepoError), e: qtlib.WarningMsgBox(_('Unable to read repository status'), uni(str(e)), parent=self) except util.Abort, e: if e.hint: err = _('%s (hint: %s)') % (uni(str(e)), uni(e.hint)) else: err = uni(str(e)) qtlib.WarningMsgBox(_('Unable to read repository status'), err, parent=self) self.lclunknowns = [] return if not self.pats: try: self.pats = [self.lclunknowns[i.row()] for i in self.unknownlist.selectedIndexes()] except IndexError: self.pats = [] self.lclunknowns = wctx.unknown() self.unknownlist.clear() self.unknownlist.addItems([uni(u) for u in self.lclunknowns]) for i, u in enumerate(self.lclunknowns): if u in self.pats: item = self.unknownlist.item(i) self.unknownlist.setItemSelected(item, True) self.unknownlist.setCurrentItem(item) self.le.setText(QString(u)) self.pats = [] def writeIgnoreFile(self): eol = self.doseoln and '\r\n' or '\n' out = eol.join(self.ignorelines) + eol hasignore = os.path.exists(self.repo.join(self.ignorefile)) try: f = util.atomictempfile(self.ignorefile, 'wb', createmode=None) f.write(out) f.close() if not hasignore: ret = qtlib.QuestionMsgBox(_('New file created'), _('TortoiseHg has created a new ' '.hgignore file. Would you like to ' 'add this file to the source code ' 'control repository?'), parent=self) if ret: commands.add(ui.ui(), self.repo, self.ignorefile) shlib.shell_notify([self.ignorefile]) self.ignoreFilterUpdated.emit() except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write .hgignore file'), hglib.tounicode(str(e)), parent=self) def accept(self): s = QSettings() s.setValue('hgignore/geom', self.saveGeometry()) QDialog.accept(self) def reject(self): s = QSettings() s.setValue('hgignore/geom', self.saveGeometry()) QDialog.reject(self) tortoisehg-2.10/tortoisehg/hgqt/repotreeitem.py0000644000076400007640000004207512235634453021120 0ustar stevesteve# repotreeitem.py - treeitems for the reporegistry # # Copyright 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os from mercurial import node from mercurial import ui, hg, util, error from tortoisehg.util import hglib, paths from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, hgrcutil from PyQt4.QtCore import * from PyQt4.QtGui import * def _dumpChild(xw, parent): for c in parent.childs: c.dumpObject(xw) def undumpObject(xr): xmltagname = str(xr.name().toString()) obj = _xmlUndumpMap[xmltagname](xr) assert obj.xmltagname == xmltagname return obj def _undumpChild(xr, parent, undump=undumpObject): while not xr.atEnd(): xr.readNext() if xr.isStartElement(): try: item = undump(xr) parent.appendChild(item) except KeyError: pass # ignore unknown classes in xml elif xr.isEndElement(): break def flatten(root, stopfunc=None): """Iterate root and its child items recursively until stop condition""" yield root if stopfunc and stopfunc(root): return for c in root.childs: for e in flatten(c, stopfunc): yield e def find(root, targetfunc, stopfunc=None): """Search recursively for item of which targetfunc evaluates to True""" for e in flatten(root, stopfunc): if targetfunc(e): return e raise ValueError('not found') class RepoTreeItem(object): xmltagname = 'treeitem' def __init__(self, parent=None): self._parent = parent self.childs = [] self._row = 0 def appendChild(self, child): child._row = len(self.childs) child._parent = self self.childs.append(child) def insertChild(self, row, child): child._row = row child._parent = self self.childs.insert(row, child) def child(self, row): return self.childs[row] def childCount(self): return len(self.childs) def columnCount(self): return 2 def data(self, column, role): return QVariant() def setData(self, column, value): return False def row(self): return self._row def parent(self): return self._parent def menulist(self): return [] def flags(self): return Qt.NoItemFlags def removeRows(self, row, count): cs = self.childs remove = cs[row : row + count] keep = cs[:row] + cs[row + count:] self.childs = keep for c in remove: c._row = 0 c._parent = None for i, c in enumerate(keep): c._row = i return True def dump(self, xw): _dumpChild(xw, parent=self) @classmethod def undump(cls, xr): obj = cls() _undumpChild(xr, parent=obj) return obj def dumpObject(self, xw): xw.writeStartElement(self.xmltagname) self.dump(xw) xw.writeEndElement() def isRepo(self): return False def details(self): return '' def okToDelete(self): return True def getSupportedDragDropActions(self): return Qt.MoveAction class RepoItem(RepoTreeItem): xmltagname = 'repo' def __init__(self, root, shortname=None, basenode=None, parent=None): RepoTreeItem.__init__(self, parent) self._root = root self._shortname = shortname or u'' self._basenode = basenode or node.nullid self._valid = True # expensive check is done at appendSubrepos() def isRepo(self): return True # TODO: return unicode instead of localstr because shortname() is unicode def rootpath(self): return self._root def shortname(self): if self._shortname: return self._shortname else: return hglib.tounicode(os.path.basename(self._root)) def repotype(self): return 'hg' def basenode(self): """Return node id of revision 0""" return self._basenode def setBaseNode(self, basenode): self._basenode = basenode def setShortName(self, uname): uname = unicode(uname) if uname != self._shortname: self._shortname = uname def data(self, column, role): if role == Qt.DecorationRole and column == 0: baseiconname = 'hg' if paths.is_unc_path(self.rootpath()): baseiconname = 'thg-remote-repo' ico = qtlib.geticon(baseiconname) if not self._valid: ico = qtlib.getoverlaidicon(ico, qtlib.geticon('dialog-warning')) return ico elif role in (Qt.DisplayRole, Qt.EditRole): return [self.shortname, self.shortpath][column]() def getCommonPath(self): return self.parent().getCommonPath() def shortpath(self): try: cpath = self.getCommonPath() except: cpath = '' spath2 = spath = os.path.normpath(self._root) if os.name == 'nt': spath2 = spath2.lower() if cpath and spath2.startswith(cpath): iShortPathStart = len(cpath) spath = spath[iShortPathStart:] if spath and spath[0] in '/\\': # do not show a slash at the beginning of the short path spath = spath[1:] return hglib.tounicode(spath) def menulist(self): acts = ['open', 'clone', 'addsubrepo', None, 'explore', 'terminal', 'copypath', None, 'rename', 'remove'] if self.childCount() > 0: acts.extend([None, (_('&Sort'), ['sortbyname', 'sortbyhgsub'])]) acts.extend([None, 'settings']) return acts def flags(self): return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEditable) def dump(self, xw): xw.writeAttribute('root', hglib.tounicode(self._root)) xw.writeAttribute('shortname', self.shortname()) xw.writeAttribute('basenode', node.hex(self.basenode())) _dumpChild(xw, parent=self) @classmethod def undump(cls, xr): a = xr.attributes() obj = cls(hglib.fromunicode(a.value('', 'root').toString()), unicode(a.value('', 'shortname').toString()), node.bin(str(a.value('', 'basenode').toString()))) _undumpChild(xr, parent=obj, undump=_undumpSubrepoItem) return obj def details(self): return _('Local Repository %s') % hglib.tounicode(self._root) def appendSubrepos(self, repo=None): invalidRepoList = [] try: sri = None if repo is None: if not os.path.exists(self._root): self._valid = False return [self._root] elif not os.path.exists(os.path.join(self._root, '.hgsub')): return [] # skip repo creation, which is expensive repo = hg.repository(ui.ui(), self._root) wctx = repo['.'] sortkey = lambda x: os.path.basename(util.normpath(repo.wjoin(x))) for subpath in sorted(wctx.substate, key=sortkey): sri = None abssubpath = repo.wjoin(subpath) subtype = wctx.substate[subpath][2] sriIsValid = os.path.isdir(abssubpath) sri = _newSubrepoItem(abssubpath, repotype=subtype) sri._valid = sriIsValid self.appendChild(sri) if not sriIsValid: self._valid = False sri._valid = False invalidRepoList.append(repo.wjoin(subpath)) return invalidRepoList if subtype == 'hg': # Only recurse into mercurial subrepos sctx = wctx.sub(subpath) invalidSubrepoList = sri.appendSubrepos(sctx._repo) if invalidSubrepoList: self._valid = False invalidRepoList += invalidSubrepoList except (EnvironmentError, error.RepoError, util.Abort), e: # Add the repo to the list of repos/subrepos # that could not be open self._valid = False if sri: sri._valid = False invalidRepoList.append(abssubpath) invalidRepoList.append(self._root) except Exception, e: # If any other sort of exception happens, show the corresponding # error message, but do not crash! # Note that we _also_ will mark the offending repos as invalid # It is unfortunate that Python 2.4, which we target does not # support combined try/except/finally clauses, forcing us # to duplicate some code here self._valid = False if sri: sri._valid = False invalidRepoList.append(abssubpath) invalidRepoList.append(self._root) # Show a warning message indicating that there was an error if repo: rootpath = repo.root else: rootpath = self._root warningMessage = (_('An exception happened while loading the ' \ 'subrepos of:

    "%s"

    ') + \ _('The exception error message was:

    %s

    ') +\ _('Click OK to continue or Abort to exit.')) \ % (hglib.tounicode(rootpath), hglib.tounicode(e.message)) res = qtlib.WarningMsgBox(_('Error loading subrepos'), warningMessage, buttons = QMessageBox.Ok | QMessageBox.Abort) # Let the user abort so that he gets the full exception info if res == QMessageBox.Abort: raise return invalidRepoList def setData(self, column, value): if column == 0: shortname = hglib.fromunicode(value.toString()) abshgrcpath = os.path.join(self.rootpath(), '.hg', 'hgrc') if not hgrcutil.setConfigValue(abshgrcpath, 'web.name', shortname): qtlib.WarningMsgBox(_('Unable to update repository name'), _('An error occurred while updating the repository hgrc ' 'file (%s)') % hglib.tounicode(abshgrcpath)) return False self.setShortName(value.toString()) return True return False _subrepoType2IcoMap = { 'hg': 'hg', 'git': 'thg-git-subrepo', 'svn': 'thg-svn-subrepo', } def _newSubrepoIcon(repotype, valid=True): subiconame = _subrepoType2IcoMap.get(repotype) if subiconame is None: ico = qtlib.geticon('thg-subrepo') else: ico = qtlib.geticon(subiconame) ico = qtlib.getoverlaidicon(ico, qtlib.geticon('thg-subrepo')) if not valid: ico = qtlib.getoverlaidicon(ico, qtlib.geticon('dialog-warning')) return ico class StandaloneSubrepoItem(RepoItem): """Mercurial repository just decorated as subrepo""" xmltagname = 'subrepo' def data(self, column, role): if role == Qt.DecorationRole and column == 0: return _newSubrepoIcon('hg', valid=self._valid) else: return super(StandaloneSubrepoItem, self).data(column, role) class SubrepoItem(RepoItem): """Actual Mercurial subrepo""" xmltagname = 'subrepo' def data(self, column, role): if role == Qt.DecorationRole and column == 0: return _newSubrepoIcon('hg', valid=self._valid) else: return super(SubrepoItem, self).data(column, role) def menulist(self): acts = ['open', 'clone', None, 'addsubrepo', 'removesubrepo', None, 'explore', 'terminal', 'copypath'] if self.childCount() > 0: acts.extend([None, (_('&Sort'), ['sortbyname', 'sortbyhgsub'])]) acts.extend([None, 'settings']) return acts def getSupportedDragDropActions(self): return Qt.CopyAction def flags(self): return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled # possibly this should not be a RepoItem because it lacks common functions class AlienSubrepoItem(RepoItem): """Actual non-Mercurial subrepo""" xmltagname = 'subrepo' def __init__(self, root, repotype, parent=None): RepoItem.__init__(self, root, parent=parent) self._repotype = repotype def data(self, column, role): if role == Qt.DecorationRole and column == 0: return _newSubrepoIcon(self._repotype) else: return super(AlienSubrepoItem, self).data(column, role) def menulist(self): return ['explore', 'terminal', 'copypath'] def flags(self): return Qt.ItemIsEnabled | Qt.ItemIsSelectable def repotype(self): return self._repotype def dump(self, xw): xw.writeAttribute('root', hglib.tounicode(self._root)) xw.writeAttribute('repotype', self._repotype) @classmethod def undump(cls, xr): a = xr.attributes() obj = cls(hglib.fromunicode(a.value('', 'root').toString()), str(a.value('', 'repotype').toString())) xr.skipCurrentElement() # no child return obj def appendSubrepos(self, repo=None): raise Exception('unsupported by non-hg subrepo') def _newSubrepoItem(root, repotype): if repotype == 'hg': return SubrepoItem(root) else: return AlienSubrepoItem(root, repotype=repotype) def _undumpSubrepoItem(xr): a = xr.attributes() repotype = str(a.value('', 'repotype').toString()) or 'hg' if repotype == 'hg': return SubrepoItem.undump(xr) else: return AlienSubrepoItem.undump(xr) class RepoGroupItem(RepoTreeItem): xmltagname = 'group' def __init__(self, name, parent=None): RepoTreeItem.__init__(self, parent) self.name = name self._commonpath = '' def data(self, column, role): if role == Qt.DecorationRole: if column == 0: s = QApplication.style() ico = s.standardIcon(QStyle.SP_DirIcon) return QVariant(ico) return QVariant() if column == 0: return QVariant(self.name) elif column == 1: return QVariant(self.getCommonPath()) return QVariant() def setData(self, column, value): if column == 0: self.name = value.toString() return True return False def rootpath(self): # for sortbypath() return '' # may be okay to return _commonpath instead? def shortname(self): # for sortbyname() return unicode(self.name) def menulist(self): return ['openAll', 'add', None, 'newGroup', None, 'rename', 'remove', None, (_('&Sort'), ['sortbyname', 'sortbypath']), None, 'reloadRegistry'] def flags(self): return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsEditable) def childRoots(self): return [c._root for c in self.childs if isinstance(c, RepoItem)] def dump(self, xw): xw.writeAttribute('name', self.name) _dumpChild(xw, parent=self) @classmethod def undump(cls, xr): a = xr.attributes() obj = cls(a.value('', 'name').toString()) _undumpChild(xr, parent=obj) return obj def okToDelete(self): return False def updateCommonPath(self, cpath=None): """ Update or set the group 'common path' When called with no arguments, the group common path is calculated by looking for the common path of all the repos on a repo group When called with an argument, the group common path is set to the input argument. This is commonly used to set the group common path to an empty string, thus disabling the "show short paths" functionality. """ if not cpath is None: self._commonpath = cpath elif len(self.childs) == 0: # If a group has no repo items, the common path is empty self._commonpath = '' else: childs = [os.path.normcase(child.rootpath()) for child in self.childs if not isinstance(child, RepoGroupItem)] self._commonpath = os.path.dirname(os.path.commonprefix(childs)) return self._commonpath def getCommonPath(self): return self._commonpath class AllRepoGroupItem(RepoGroupItem): xmltagname = 'allgroup' def __init__(self, name=None, parent=None): RepoGroupItem.__init__(self, name or _('default'), parent=parent) def menulist(self): return ['openAll', 'add', None, 'newGroup', None, 'rename', None, (_('&Sort'), ['sortbyname', 'sortbypath']), None, 'reloadRegistry'] _xmlUndumpMap = { 'allgroup': AllRepoGroupItem.undump, 'group': RepoGroupItem.undump, 'repo': RepoItem.undump, 'subrepo': StandaloneSubrepoItem.undump, 'treeitem': RepoTreeItem.undump, } tortoisehg-2.10/tortoisehg/hgqt/modeltest.py0000644000076400007640000005122412235634453020410 0ustar stevesteve############################################################################# ## ## Copyright (C) 2007 Trolltech ASA. All rights reserved. ## ## This file is part of the Qt Concurrent project on Trolltech Labs. ## ## This file may be used under the terms of the GNU General Public ## License version 2.0 as published by the Free Software Foundation ## and appearing in the file LICENSE.GPL included in the packaging of ## this file. Please review the following information to ensure GNU ## General Public Licensing requirements will be met: ## http://www.trolltech.com/products/qt/opensource.html ## ## If you are unsure which license is appropriate for your use, please ## review the following information: ## http://www.trolltech.com/products/qt/licensing.html or contact the ## sales department at sales@trolltech.com. ## ## This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ## WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ## ############################################################################# import sip from PyQt4 import QtCore, QtGui # This was originally a Trolltech file. The QBzr folks did some work to # bring it up to date, and I've done some work on top of theirs to fix a few # minor bugs. # # To test a model, instantiate this class with a reference to the model and # its parent # from modeltest import ModelTest # model = MyFancyModel(self) # self.modeltest = ModelTest(model, self) class ModelTest(QtCore.QObject): def __init__(self, _model, parent=None): """ Connect to all of the models signals, Whenever anything happens recheck everything. """ QtCore.QObject.__init__(self,parent) self._model = _model self.model = sip.cast(_model, QtCore.QAbstractItemModel) self.insert = [] self.remove = [] self.fetchingMore = False assert(self.model) self.connect( self.model, QtCore.SIGNAL("columnsAboutToBeInserted(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("columnsAboutToBeRemoved(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("columnsBeInserted(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("columnsRemoved(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("dataChanged(const QModelIndex&, const QModelIndex&)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("headerDataChanged(Qt::Orientation, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("layoutAboutToBeChanged()"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("layoutChanged()"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("modelReset()"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("rowsAboutToBeInserted(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("rowsAboutToBeRemoved(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("rowsBeInserted(const QModelIndex&, int, int)"), self.runAllTests) self.connect( self.model, QtCore.SIGNAL("rowsRemoved(const QModelIndex&, int, int)"), self.runAllTests) # Special checks for inserting/removing self.connect( self.model, QtCore.SIGNAL("rowsAboutToBeInserted(const QModelIndex&, int, int)"), self.rowsAboutToBeInserted) self.connect( self.model, QtCore.SIGNAL("rowsAboutToBeRemoved(const QModelIndex&, int, int)"), self.rowsAboutToBeRemoved) self.connect( self.model, QtCore.SIGNAL("rowsBeInserted(const QModelIndex&, int, int)"), self.rowsInserted) self.connect( self.model, QtCore.SIGNAL("rowsRemoved(const QModelIndex&, int, int)"), self.rowsRemoved) self.runAllTests() def nonDestructiveBasicTest(self): """ nonDestructiveBasicTest tries to call a number of the basic functions (not all) to make sure the model doesn't outright segfault, testing the functions that makes sense. """ assert(self.model.buddy(QtCore.QModelIndex()) == QtCore.QModelIndex()) self.model.canFetchMore(QtCore.QModelIndex()) assert(self.model.columnCount(QtCore.QModelIndex()) >= 0) assert(self.model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) == QtCore.QVariant()) self.fetchingMore = True self.model.fetchMore(QtCore.QModelIndex()) self.fetchingMore = False flags = self.model.flags(QtCore.QModelIndex()) assert( int(flags & QtCore.Qt.ItemIsEnabled) == QtCore.Qt.ItemIsEnabled or int(flags & QtCore.Qt.ItemIsEnabled ) == 0 ) self.model.hasChildren(QtCore.QModelIndex()) self.model.hasIndex(0,0) self.model.headerData(0,QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) self.model.index(0,0, QtCore.QModelIndex()) self.model.itemData(QtCore.QModelIndex()) cache = QtCore.QVariant() self.model.match(QtCore.QModelIndex(), -1, cache) self.model.mimeTypes() assert(self.model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex()) assert(self.model.rowCount(QtCore.QModelIndex()) >= 0) variant = QtCore.QVariant() self.model.setData(QtCore.QModelIndex(), variant, -1) self.model.setHeaderData(-1, QtCore.Qt.Horizontal, QtCore.QVariant()) self.model.setHeaderData(0, QtCore.Qt.Horizontal, QtCore.QVariant()) self.model.setHeaderData(999999, QtCore.Qt.Horizontal, QtCore.QVariant()) self.model.sibling(0,0,QtCore.QModelIndex()) self.model.span(QtCore.QModelIndex()) self.model.supportedDropActions() def rowCount(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::rowCount() and hasChildren() self.models that are dynamically populated are not as fully tested here. """ # check top row topindex = self.model.index(0,0,QtCore.QModelIndex()) rows = self.model.rowCount(topindex) assert(rows >= 0) if rows > 0: assert(self.model.hasChildren(topindex) == True ) secondlvl = self.model.index(0,0,topindex) if secondlvl.isValid(): # check a row count where parent is valid rows = self.model.rowCount(secondlvl) assert(rows >= 0) if rows > 0: assert(self.model.hasChildren(secondlvl) == True) # The self.models rowCount() is tested more extensively in checkChildren, # but this catches the big mistakes def columnCount(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::columnCount() and hasChildren() """ # check top row topidx = self.model.index(0,0,QtCore.QModelIndex()) assert(self.model.columnCount(topidx) >= 0) # check a column count where parent is valid childidx = self.model.index(0,0,topidx) if childidx.isValid() : assert(self.model.columnCount(childidx) >= 0) # columnCount() is tested more extensively in checkChildren, # but this catches the big mistakes def hasIndex(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::hasIndex() """ # Make sure that invalid values returns an invalid index assert(self.model.hasIndex(-2,-2) == False) assert(self.model.hasIndex(-2,0) == False) assert(self.model.hasIndex(0,-2) == False) rows = self.model.rowCount(QtCore.QModelIndex()) cols = self.model.columnCount(QtCore.QModelIndex()) # check out of bounds assert(self.model.hasIndex(rows,cols) == False) assert(self.model.hasIndex(rows+1,cols+1) == False) if rows > 0: assert(self.model.hasIndex(0,0) == True) # hasIndex() is tested more extensively in checkChildren() # but this catches the big mistakes def index(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::index() """ # Make sure that invalid values returns an invalid index assert(self.model.index(-2,-2, QtCore.QModelIndex()) == QtCore.QModelIndex()) assert(self.model.index(-2,0, QtCore.QModelIndex()) == QtCore.QModelIndex()) assert(self.model.index(0,-2, QtCore.QModelIndex()) == QtCore.QModelIndex()) rows = self.model.rowCount(QtCore.QModelIndex()) cols = self.model.columnCount(QtCore.QModelIndex()) if rows == 0: return # Catch off by one errors assert(self.model.index(rows,cols, QtCore.QModelIndex()) == QtCore.QModelIndex()) assert(self.model.index(0,0, QtCore.QModelIndex()).isValid() == True) # Make sure that the same index is *always* returned a = self.model.index(0,0, QtCore.QModelIndex()) b = self.model.index(0,0, QtCore.QModelIndex()) assert(a==b) # index() is tested more extensively in checkChildren() # but this catches the big mistakes def parent(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::parent() """ # Make sure the self.model wont crash and will return an invalid QtCore.QModelIndex # when asked for the parent of an invalid index assert(self.model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex()) if self.model.rowCount(QtCore.QModelIndex()) == 0: return; # Column 0 | Column 1 | # QtCore.Qself.modelIndex() | | # \- topidx | topidx1 | # \- childix | childidx1 | # Common error test #1, make sure that a top level index has a parent # that is an invalid QtCore.Qself.modelIndex topidx = self.model.index(0,0,QtCore.QModelIndex()) assert(self.model.parent(topidx) == QtCore.QModelIndex()) # Common error test #2, make sure that a second level index has a parent # that is the first level index if self.model.rowCount(topidx) > 0 : childidx = self.model.index(0,0,topidx) assert(self.model.parent(childidx) == topidx) # Common error test #3, the second column should NOT have the same children # as the first column in a row # Usually the second column shouldn't have children topidx1 = self.model.index(0,1,QtCore.QModelIndex()) if self.model.rowCount(topidx1) > 0: childidx = self.model.index(0,0,topidx) childidx1 = self.model.index(0,0,topidx1) assert(childidx != childidx1) # Full test, walk n levels deep through the self.model making sure that all # parent's children correctly specify their parent self.checkChildren(QtCore.QModelIndex()) def data(self): """ Tests self.model's implementation of QtCore.QAbstractItemModel::data() """ # Invalid index should return an invalid qvariant assert( not self.model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole).isValid()) if self.model.rowCount(QtCore.QModelIndex()) == 0: return # A valid index should have a valid QtCore.QVariant data assert( self.model.index(0,0, QtCore.QModelIndex()).isValid()) # shouldn't be able to set data on an invalid index assert( self.model.setData( QtCore.QModelIndex(), QtCore.QVariant("foo"), QtCore.Qt.DisplayRole) == False) # General Purpose roles that should return a QString variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.ToolTipRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.String ) ) variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.StatusTipRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.String ) ) variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.WhatsThisRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.String ) ) # General Purpose roles that should return a QSize variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.SizeHintRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.Size ) ) # General Purpose roles that should return a QFont variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.FontRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.Font ) ) # Check that the alignment is one we know about variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.TextAlignmentRole) if variant.isValid(): alignment = variant.toInt()[0] assert( alignment == (alignment & int(QtCore.Qt.AlignHorizontal_Mask | QtCore.Qt.AlignVertical_Mask))) # General Purpose roles that should return a QColor variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.BackgroundColorRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.Color ) ) variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.TextColorRole) if variant.isValid(): assert( variant.canConvert( QtCore.QVariant.Color ) ) # Check that the "check state" is one we know about. variant = self.model.data(self.model.index(0,0,QtCore.QModelIndex()), QtCore.Qt.CheckStateRole) if variant.isValid(): state = variant.toInt()[0] assert( state == QtCore.Qt.Unchecked or state == QtCore.Qt.PartiallyChecked or state == QtCore.Qt.Checked ) def runAllTests(self): if self.fetchingMore: return self.nonDestructiveBasicTest() self.rowCount() self.columnCount() self.hasIndex() self.index() self.parent() self.data() def rowsAboutToBeInserted(self, parent, start, end): """ Store what is about to be inserted to make sure it actually happens """ c = {} c['parent'] = parent c['oldSize'] = self.model.rowCount(parent) c['last'] = self.model.data(self.model.index(start-1, 0, parent)) c['next'] = self.model.data(self.model.index(start, 0, parent)) self.insert.append(c) def rowsInserted(self, parent, start, end): """ Confirm that what was said was going to happen actually did """ c = self.insert.pop() assert(c['parent'] == parent) assert(c['oldSize'] + (end - start + 1) == self.model.rowCount(parent)) assert(c['last'] == self.model.data(self.model.index(start-1, 0, c['parent']))) # if c['next'] != self.model.data(model.index(end+1, 0, c['parent'])): # qDebug << start << end # for i in range(0, self.model.rowCount(QtCore.QModelIndex())): # qDebug << self.model.index(i, 0).data().toString() # qDebug() << c['next'] << self.model.data(model.index(end+1, 0, c['parent'])) assert(c['next'] == self.model.data(self.model.index(end+1, 0, c['parent']))) def rowsAboutToBeRemoved(self, parent, start, end): """ Store what is about to be inserted to make sure it actually happens """ c = {} c['parent'] = parent c['oldSize'] = self.model.rowCount(parent) c['last'] = self.model.data(self.model.index(start-1, 0, parent)) c['next'] = self.model.data(self.model.index(end+1, 0, parent)) self.remove.append(c) def rowsRemoved(self, parent, start, end): """ Confirm that what was said was going to happen actually did """ c = self.remove.pop() assert(c['parent'] == parent) assert(c['oldSize'] - (end - start + 1) == self.model.rowCount(parent)) assert(c['last'] == self.model.data(self.model.index(start-1, 0, c['parent']))) assert(c['next'] == self.model.data(self.model.index(start, 0, c['parent']))) def checkChildren(self, parent, depth = 0): """ Called from parent() test. A self.model that returns an index of parent X should also return X when asking for the parent of the index This recursive function does pretty extensive testing on the whole self.model in an effort to catch edge cases. This function assumes that rowCount(QtCore.QModelIndex()), columnCount(QtCore.QModelIndex()) and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one """ # First just try walking back up the tree. p = parent; while p.isValid(): p = p.parent() #For self.models that are dynamically populated if self.model.canFetchMore( parent ): self.fetchingMore = True self.model.fetchMore(parent) self.fetchingMore = False rows = self.model.rowCount(parent) cols = self.model.columnCount(parent) if rows > 0: assert(self.model.hasChildren(parent)) # Some further testing against rows(), columns, and hasChildren() assert( rows >= 0 ) assert( cols >= 0 ) if rows > 0: assert(self.model.hasChildren(parent) == True) # qDebug() << "parent:" << self.model.data(parent).toString() << "rows:" << rows # << "columns:" << cols << "parent column:" << parent.column() assert( self.model.hasIndex( rows+1, 0, parent) == False) for r in range(0,rows): if self.model.canFetchMore(parent): self.fetchingMore = True self.model.fetchMore(parent) self.fetchingMore = False assert(self.model.hasIndex(r,cols+1,parent) == False) for c in range(0,cols): assert(self.model.hasIndex(r,c,parent)) index = self.model.index(r,c,parent) # rowCount(QtCore.QModelIndex()) and columnCount(QtCore.QModelIndex()) said that it existed... assert(index.isValid() == True) # index() should always return the same index when called twice in a row modIdx = self.model.index(r,c,parent) assert(index == modIdx) # Make sure we get the same index if we request it twice in a row a = self.model.index(r,c,parent) b = self.model.index(r,c,parent) assert( a == b ) # Some basic checking on the index that is returned # assert( index.model() == self.model ) # This raises an error that is not part of the qbzr code. # see http://www.opensubscriber.com/message/pyqt@riverbankcomputing.com/10335500.html assert( index.row() == r ) assert( index.column() == c ) # While you can technically return a QtCore.QVariant usually this is a sign # if an bug in data() Disable if this really is ok in your self.model assert( self.model.data(index, QtCore.Qt.DisplayRole).isValid() == True ) #if the next test fails here is some somehwat useful debug you play with # if self.model.parent(index) != parent: # qDebug() << r << c << depth << self.model.data(index).toString() # << self.model.data(parent).toString() # qDebug() << index << parent << self.model.parent(index) # # And a view that you can even use to show the self.model # # view = QtGui.QTreeView() # # view.setself.model(model) # # view.show() # # Check that we can get back our real parent p = self.model.parent( index ) assert( p.internalId() == parent.internalId() ) assert( p.row() == parent.row() ) # recursively go down the children if self.model.hasChildren(index) and depth < 10: # qDebug() << r << c << "hasChildren" << self.model.rowCount(index) depth += 1 self.checkChildren(index, depth) #else: # if depth >= 10: # qDebug() << "checked 10 deep" # Make sure that after testing the children that the index doesn't change newIdx = self.model.index(r,c,parent) assert(index == newIdx) tortoisehg-2.10/tortoisehg/hgqt/compress.py0000644000076400007640000001211412231647662020240 0ustar stevesteve# compress.py - History compression dialog for TortoiseHg # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import revset from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import csinfo, cmdui, commit, wctxcleaner BB = QDialogButtonBox class CompressDialog(QDialog): showMessage = pyqtSignal(QString) def __init__(self, repoagent, revs, parent): super(CompressDialog, self).__init__(parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent self.revs = revs box = QVBoxLayout() box.setSpacing(8) box.setContentsMargins(*(6,)*4) self.setLayout(box) style = csinfo.panelstyle(selectable=True) srcb = QGroupBox( _('Compress changesets up to and including')) srcb.setLayout(QVBoxLayout()) srcb.layout().setContentsMargins(*(2,)*4) source = csinfo.create(self.repo, revs[0], style, withupdate=True) srcb.layout().addWidget(source) self.layout().addWidget(srcb) destb = QGroupBox( _('Onto destination')) destb.setLayout(QVBoxLayout()) destb.layout().setContentsMargins(*(2,)*4) dest = csinfo.create(self.repo, revs[1], style, withupdate=True) destb.layout().addWidget(dest) self.destcsinfo = dest self.layout().addWidget(destb) self.cmd = cmdui.Widget(True, True, self) self.cmd.commandFinished.connect(self.commandFinished) self.cmd.setShowOutput(True) self.showMessage.connect(self.cmd.stbar.showMessage) self.layout().addWidget(self.cmd, 2) bbox = QDialogButtonBox() self.cancelbtn = bbox.addButton(QDialogButtonBox.Cancel) self.cancelbtn.clicked.connect(self.reject) self.compressbtn = bbox.addButton(_('Compress'), QDialogButtonBox.ActionRole) self.compressbtn.clicked.connect(self.compress) self.layout().addWidget(bbox) self.bbox = bbox self.showMessage.emit(_('Checking...')) self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkFinished.connect(self._checkCompleted) self.cmd.stbar.linkActivated.connect(self._wctxcleaner.runCleaner) QTimer.singleShot(0, self._wctxcleaner.check) self.setMinimumWidth(480) self.setMaximumHeight(800) self.resize(0, 340) repo = repoagent.rawRepo() self.setWindowTitle(_('Compress - %s') % repo.displayname) self.restoreSettings() @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot(bool) def _checkCompleted(self, clean): if not clean: self.compressbtn.setEnabled(False) txt = _('Before compress, you must ' 'commit or ' 'discard changes.') else: self.compressbtn.setEnabled(True) txt = _('You may continue the compress') self.showMessage.emit(txt) def compress(self): self.cancelbtn.setShown(False) uc = ['update', '--repository', self.repo.root, '--clean', '--rev', str(self.revs[1])] rc = ['revert', '--repository', self.repo.root, '--all', '--rev', str(self.revs[0])] self.repo.incrementBusyCount() self.cmd.run(uc, rc) def commandFinished(self, ret): self.repo.decrementBusyCount() self.showMessage.emit(_('Changes have been moved, you must now commit')) self.compressbtn.setText(_('Commit', 'action button')) self.compressbtn.clicked.disconnect(self.compress) self.compressbtn.clicked.connect(self.commit) def commit(self): tip, base = self.revs func = revset.match(self.repo.ui, '%s::%s' % (base, tip)) revcount = len(self.repo) revs = [c for c in func(self.repo, range(revcount)) if c != base] descs = [self.repo[c].description() for c in revs] self.repo.opener('cur-message.txt', 'w').write('\n* * *\n'.join(descs)) dlg = commit.CommitDialog(self._repoagent, [], {}, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.showMessage.emit(_('Compress is complete, old history untouched')) self.compressbtn.setText(_('Close')) self.compressbtn.clicked.disconnect(self.commit) self.compressbtn.clicked.connect(self.accept) def storeSettings(self): s = QSettings() s.setValue('compress/geometry', self.saveGeometry()) def restoreSettings(self): s = QSettings() self.restoreGeometry(s.value('compress/geometry').toByteArray()) def accept(self): self.storeSettings() super(CompressDialog, self).accept() def reject(self): self.storeSettings() super(CompressDialog, self).reject() tortoisehg-2.10/tortoisehg/hgqt/repofilter.py0000644000076400007640000004211612231647662020565 0ustar stevesteve# repofilter.py - TortoiseHg toolbar for filtering changesets # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright (C) 2010 Yuya Nishihara # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error, revset as hgrevset from mercurial import repoview from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import revset, qtlib _permanent_queries = ('head()', 'merge()', 'tagged()', 'bookmark()', 'file(".hgsubstate") or file(".hgsub")') def _firstword(query): try: for token, value, _pos in hgrevset.tokenize(hglib.fromunicode(query)): if token == 'symbol' or token == 'string': return value # localstr except error.ParseError: pass def _querytype(repo, query): """ >>> repo = set('0 1 2 3 . stable'.split()) >>> _querytype(repo, u'') is None True >>> _querytype(repo, u'quick fox') 'keyword' >>> _querytype(repo, u'0') 'revset' >>> _querytype(repo, u'stable') 'revset' >>> _querytype(repo, u'0::2') # symbol 'revset' >>> _querytype(repo, u'::"stable"') # string 'revset' >>> _querytype(repo, u'"') # unterminated string 'keyword' >>> _querytype(repo, u'tagged()') 'revset' """ if not query: return if '(' in query: return 'revset' changeid = _firstword(query) if not changeid: return 'keyword' try: if changeid in repo: return 'revset' except error.LookupError: # ambiguous changeid pass return 'keyword' class RepoFilterBar(QToolBar): """Toolbar for RepoWidget to filter changesets""" setRevisionSet = pyqtSignal(object) clearRevisionSet = pyqtSignal() filterToggled = pyqtSignal(bool) showMessage = pyqtSignal(QString) progress = pyqtSignal(QString, object, QString, QString, object) branchChanged = pyqtSignal(unicode, bool) """Emitted (branch, allparents) when branch selection changed""" showHiddenChanged = pyqtSignal(bool) showGraftSourceChanged = pyqtSignal(bool) _allBranchesLabel = u'\u2605 ' + _('Show all') + u' \u2605' def __init__(self, repo, parent=None): super(RepoFilterBar, self).__init__(parent) self.layout().setContentsMargins(0, 0, 0, 0) self.setIconSize(QSize(16,16)) self.setFloatable(False) self.setMovable(False) self._repo = repo self._permanent_queries = list(_permanent_queries) username = repo.ui.config('ui', 'username') if username: self._permanent_queries.insert(0, hgrevset.formatspec('author(%s)', os.path.expandvars(username))) self.filterEnabled = True #Check if the font contains the glyph needed by the branch combo if not QFontMetrics(self.font()).inFont(QString(u'\u2605').at(0)): self._allBranchesLabel = u'*** %s ***' % _('Show all') self.entrydlg = revset.RevisionSetQuery(repo, self) self.entrydlg.progress.connect(self.progress) self.entrydlg.showMessage.connect(self.showMessage) self.entrydlg.queryIssued.connect(self.queryIssued) self.entrydlg.hide() self.revsetcombo = combo = QComboBox() combo.setEditable(True) combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) combo.setMinimumContentsLength(10) qtlib.allowCaseChangingInput(combo) le = combo.lineEdit() le.returnPressed.connect(self.runQuery) le.selectionChanged.connect(self.selectionChanged) if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### revision set query ###')) combo.activated.connect(self.runQuery) self._revsettypelabel = QLabel(le) self._revsettypetimer = QTimer(self, interval=200, singleShot=True) self._revsettypetimer.timeout.connect(self._updateQueryType) combo.editTextChanged.connect(self._revsettypetimer.start) self._updateQueryType() le.installEventFilter(self) self.clearBtn = QToolButton(self) self.clearBtn.setIcon(qtlib.geticon('filedelete')) self.clearBtn.setToolTip(_('Clear current query and query text')) self.clearBtn.clicked.connect(self.onClearButtonClicked) self.addWidget(self.clearBtn) self.addWidget(qtlib.Spacer(2, 2)) self.addWidget(combo) self.addWidget(qtlib.Spacer(2, 2)) self.searchBtn = QToolButton(self) self.searchBtn.setIcon(qtlib.geticon('view-filter')) self.searchBtn.setToolTip(_('Trigger revision set query')) self.searchBtn.clicked.connect(self.runQuery) self.addWidget(self.searchBtn) self.editorBtn = QToolButton() self.editorBtn.setText('...') self.editorBtn.setToolTip(_('Open advanced query editor')) self.editorBtn.clicked.connect(self.openEditor) self.addWidget(self.editorBtn) icon = QIcon() icon.addPixmap(QApplication.style().standardPixmap(QStyle.SP_TrashIcon)) self.deleteBtn = QToolButton() self.deleteBtn.setIcon(icon) self.deleteBtn.setToolTip(_('Delete selected query from history')) self.deleteBtn.clicked.connect(self.deleteFromHistory) self.deleteBtn.setEnabled(False) self.addWidget(self.deleteBtn) self.addSeparator() self.filtercb = f = QCheckBox(_('filter')) f.toggled.connect(self.filterToggled) f.setToolTip(_('Toggle filtering of non-matched changesets')) self.addWidget(f) self.addSeparator() self.showHiddenBtn = QToolButton() self.showHiddenBtn.setIcon(qtlib.geticon('view-hidden')) self.showHiddenBtn.setCheckable(True) self.showHiddenBtn.setToolTip(_('Show/Hide hidden changesets')) self.showHiddenBtn.clicked.connect(self.showHiddenChanged) self.addWidget(self.showHiddenBtn) self.showGraftSourceBtn = QToolButton() self.showGraftSourceBtn.setIcon(qtlib.geticon('hg-transplant')) self.showGraftSourceBtn.setCheckable(True) self.showGraftSourceBtn.setChecked(True) self.showGraftSourceBtn.setToolTip(_('Toggle graft relations visibility')) self.showGraftSourceBtn.clicked.connect(self.showGraftSourceChanged) self.addWidget(self.showGraftSourceBtn) self.addSeparator() self._initBranchFilter() self.refresh() def onClearButtonClicked(self): if self.revsetcombo.currentText(): self.revsetcombo.clearEditText() else: self.hide() self.clearRevisionSet.emit() def setEnableFilter(self, enabled): 'Enable/disable the changing of the current filter' self.revsetcombo.setEnabled(enabled) self.clearBtn.setEnabled(enabled) self.searchBtn.setEnabled(enabled) self.editorBtn.setEnabled(enabled) self.deleteBtn.setEnabled(enabled) self._branchCombo.setEnabled(enabled) self._branchLabel.setEnabled(enabled) self.filterEnabled = enabled self.showHiddenBtn.setEnabled(enabled) self.showGraftSourceBtn.setEnabled(enabled) def selectionChanged(self): selection = self.revsetcombo.lineEdit().selectedText() self.deleteBtn.setEnabled(selection in self.revsethist) def deleteFromHistory(self): selection = self.revsetcombo.lineEdit().selectedText() if selection not in self.revsethist: return self.revsethist.remove(selection) full = self.revsethist + self._permanent_queries self.revsetcombo.clear() self.revsetcombo.addItems(full) self.revsetcombo.setCurrentIndex(-1) def showEvent(self, event): super(RepoFilterBar, self).showEvent(event) self.revsetcombo.setFocus() def eventFilter(self, watched, event): if watched is self.revsetcombo.lineEdit(): if event.type() == QEvent.Resize: self._updateQueryTypeGeometry() return False return super(RepoFilterBar, self).eventFilter(watched, event) def openEditor(self): query = self._prepareQuery() self.entrydlg.entry.setText(query) self.entrydlg.entry.setCursorPosition(0, len(query)) self.entrydlg.entry.setFocus() self.entrydlg.setShown(True) def queryIssued(self, query, revset): if self._prepareQuery() != unicode(query): # keep keyword query as-is self.revsetcombo.setEditText(query) if revset: self.setRevisionSet.emit(revset) else: self.clearRevisionSet.emit() self.saveQuery() self.revsetcombo.lineEdit().selectAll() def _prepareQuery(self): query = unicode(self.revsetcombo.currentText()).strip() if _querytype(self._repo, query) == 'keyword': s = hglib.fromunicode(query) return hglib.tounicode(hgrevset.formatspec('keyword(%s)', s)) else: return query @pyqtSlot() def _updateQueryType(self): query = unicode(self.revsetcombo.currentText()).strip() qtype = _querytype(self._repo, query) if not qtype: self._revsettypelabel.hide() self._updateQueryTypeGeometry() return name, bordercolor, bgcolor = { 'keyword': (_('Keyword Search'), '#cccccc', '#eeeeee'), 'revset': (_('Revision Set'), '#f6dd82', '#fcf1ca'), }[qtype] label = self._revsettypelabel label.setText(name) label.setStyleSheet('border: 1px solid %s; background-color: %s; ' 'color: black;' % (bordercolor, bgcolor)) label.show() self._updateQueryTypeGeometry() def _updateQueryTypeGeometry(self): le = self.revsetcombo.lineEdit() label = self._revsettypelabel # show label in right corner w = label.minimumSizeHint().width() label.setGeometry(le.width() - w - 1, 1, w, le.height() - 2) # right margin for label margins = list(le.getContentsMargins()) if label.isHidden(): margins[2] = 0 else: margins[2] = w + 1 le.setContentsMargins(*margins) def setQuery(self, query): self.revsetcombo.setEditText(query) @pyqtSlot() def runQuery(self): 'Run the current revset query or request to clear the previous result' query = self._prepareQuery() if query: self.entrydlg.entry.setText(query) self.entrydlg.runQuery() else: self.clearRevisionSet.emit() def saveQuery(self): query = self.revsetcombo.currentText() if query in self.revsethist: self.revsethist.remove(query) if query not in self._permanent_queries: self.revsethist.insert(0, query) self.revsethist = self.revsethist[:20] full = self.revsethist + self._permanent_queries self.revsetcombo.clear() self.revsetcombo.addItems(full) self.revsetcombo.setCurrentIndex(self.revsetcombo.findText(query)) def loadSettings(self, s): repoid = str(self._repo[0]) s.beginGroup('revset/' + repoid) self.entrydlg.restoreGeometry(s.value('geom').toByteArray()) self.revsethist = list(s.value('queries').toStringList()) self.filtercb.setChecked(s.value('filter', True).toBool()) full = self.revsethist + self._permanent_queries self.revsetcombo.clear() self.revsetcombo.addItems(full) self.revsetcombo.setCurrentIndex(-1) self.setVisible(s.value('showrepofilterbar').toBool()) self.showHiddenBtn.setChecked(s.value('showhidden').toBool()) self.showGraftSourceBtn.setChecked(s.value('showgraftsource', True).toBool()) self._loadBranchFilterSettings(s) s.endGroup() def saveSettings(self, s): try: repoid = str(self._repo[0]) except EnvironmentError: return s.beginGroup('revset/' + repoid) s.setValue('geom', self.entrydlg.saveGeometry()) s.setValue('queries', self.revsethist) s.setValue('filter', self.filtercb.isChecked()) s.setValue('showrepofilterbar', not self.isHidden()) self._saveBranchFilterSettings(s) s.setValue('showhidden', self.showHiddenBtn.isChecked()) s.setValue('showgraftsource', self.showGraftSourceBtn.isChecked()) s.endGroup() def _initBranchFilter(self): self._branchLabel = QToolButton( text=_('Branch'), popupMode=QToolButton.MenuButtonPopup, statusTip=_('Display graph the named branch only')) self._branchLabel.clicked.connect(self._branchLabel.showMenu) self._branchMenu = QMenu(self._branchLabel) self._abranchAction = self._branchMenu.addAction( _('Display only active branches'), self.refresh) self._abranchAction.setCheckable(True) self._cbranchAction = self._branchMenu.addAction( _('Display closed branches'), self.refresh) self._cbranchAction.setCheckable(True) self._allparAction = self._branchMenu.addAction( _('Include all ancestors'), self._emitBranchChanged) self._allparAction.setCheckable(True) self._branchLabel.setMenu(self._branchMenu) self._branchCombo = QComboBox() self._branchCombo.setMinimumContentsLength(10) self._branchCombo.setMaxVisibleItems(30) self._branchCombo.currentIndexChanged.connect(self._emitBranchChanged) self.addWidget(self._branchLabel) self.addWidget(qtlib.Spacer(2, 2)) self.addWidget(self._branchCombo) def _loadBranchFilterSettings(self, s): branch = unicode(s.value('branch').toString()) if branch == '.': branch = hglib.tounicode(self._repo.dirstate.branch()) self._branchCombo.blockSignals(True) self.setBranch(branch) self._branchCombo.blockSignals(False) self._allparAction.setChecked(s.value('branch_allparents').toBool()) def _saveBranchFilterSettings(self, s): branch = self.branch() if branch == hglib.tounicode(self._repo.dirstate.branch()): # special case for working branch: it's common to have multiple # clones which are updated to particular branches. branch = '.' s.setValue('branch', branch) s.setValue('branch_allparents', self.branchAncestorsIncluded()) def _updateBranchFilter(self): """Update the list of branches""" curbranch = self.branch() if self._abranchAction.isChecked(): branches = sorted(set([self._repo[n].branch() for n in self._repo.heads() if not self._repo[n].extra().get('close')])) elif self._cbranchAction.isChecked(): branches = sorted(self._repo.branchtags().keys()) else: branches = self._repo.namedbranches # easy access to common branches (Python sorted() is stable) priomap = {self._repo.dirstate.branch(): -2, 'default': -1} branches = sorted(branches, key=lambda e: priomap.get(e, 0)) self._branchCombo.blockSignals(True) self._branchCombo.clear() self._branchCombo.addItem(self._allBranchesLabel) for branch in branches: self._branchCombo.addItem(hglib.tounicode(branch)) self._branchCombo.setItemData(self._branchCombo.count() - 1, hglib.tounicode(branch), Qt.ToolTipRole) self._branchCombo.setEnabled(self.filterEnabled and bool(branches)) self._branchCombo.blockSignals(False) if curbranch and curbranch not in branches: self._emitBranchChanged() # falls back to "show all" else: self.setBranch(curbranch) @pyqtSlot(unicode) def setBranch(self, branch): """Change the current branch by name [unicode]""" if not branch: index = 0 else: index = self._branchCombo.findText(branch) if index >= 0: self._branchCombo.setCurrentIndex(index) def branch(self): """Return the current branch name [unicode]""" if self._branchCombo.currentIndex() == 0: return '' else: return unicode(self._branchCombo.currentText()) def branchAncestorsIncluded(self): return self._allparAction.isChecked() def getShowHidden(self): return self.showHiddenBtn.isChecked() def getShowGraftSource(self): return self.showGraftSourceBtn.isChecked() @pyqtSlot() def _emitBranchChanged(self): self.branchChanged.emit(self.branch(), self.branchAncestorsIncluded()) @pyqtSlot() def refresh(self): self._updateBranchFilter() self._updateShowHiddenBtnState() def _updateShowHiddenBtnState(self): hashidden = bool(repoview.filterrevs(self._repo, 'visible')) self.showHiddenBtn.setEnabled(hashidden) tortoisehg-2.10/tortoisehg/hgqt/pathedit.py0000664000076400007640000000252712100577421020206 0ustar stevesteve# pathedit.py # # Copyright 2010 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.hgqt.i18n import _ class PathEditDialog(QDialog): def __init__(self, parent, alias, url_): super(PathEditDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) layout = QVBoxLayout() self.setLayout(layout) self.setWindowTitle(_("Edit Repository URL")) form = QFormLayout() layout.addLayout(form) form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) self.edit = QLineEdit(url_) form.addRow(alias, self.edit) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) layout.addWidget(bb) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) self.setMinimumWidth(400) h = self.sizeHint().height() + 6 self.setMaximumHeight(h) self.setMinimumHeight(h) def accept(self): QDialog.accept(self) def reject(self): QDialog.reject(self) def url(self): return str(self.edit.text()) tortoisehg-2.10/tortoisehg/hgqt/filectxactions.py0000644000076400007640000003220512231647662021427 0ustar stevesteve# filectxactions.py - context menu actions for repository files # # Copyright 2010 Adrian Buehlmann # Copyright 2010 Steve Borho # Copyright 2012 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import util from tortoisehg.hgqt import qtlib, revert, thgrepo, visdiff, customtools from tortoisehg.hgqt.filedialogs import FileLogDialog, FileDiffDialog from tortoisehg.hgqt.i18n import _ from tortoisehg.util import hglib _actionsbytype = { 'subrepo': ['opensubrepo', 'explore', 'terminal', 'copypath', None, 'revert'], 'file': ['diff', 'ldiff', None, 'edit', 'save', None, 'ledit', 'lopen', 'copypath', None, 'revert', None, 'navigate', 'diffnavigate'], 'dir': ['diff', 'ldiff', None, 'revert', None, 'filter', None, 'explore', 'terminal', 'copypath'], } class FilectxActions(QObject): """Container for repository file actions""" linkActivated = pyqtSignal(unicode) filterRequested = pyqtSignal(QString) """Ask the repowidget to change its revset filter""" runCustomCommandRequested = pyqtSignal(str, list) def __init__(self, repo, parent=None, rev=None): super(FilectxActions, self).__init__(parent) if parent is not None and not isinstance(parent, QWidget): raise ValueError('parent must be a QWidget') self.repo = repo self.ctx = self.repo[rev] self._selectedfiles = [] # local encoding self._currentfile = None # local encoding self._itemissubrepo = False self._itemisdir = False self._nav_dialogs = qtlib.DialogKeeper(FilectxActions._createnavdialog, FilectxActions._gennavdialogkey, self) self._contextmenus = {} self._actions = {} for name, desc, icon, key, tip, cb in [ ('navigate', _('File &History'), 'hg-log', 'Shift+Return', _('Show the history of the selected file'), self.navigate), ('filter', _('Folder &History'), 'hg-log', None, _('Show the history of the selected file'), self.filterfile), ('diffnavigate', _('Co&mpare File Revisions'), 'compare-files', None, _('Compare revisions of the selected file'), self.diffNavigate), ('diff', _('&Diff to Parent'), 'visualdiff', 'Ctrl+D', _('View file changes in external diff tool'), self.vdiff), ('ldiff', _('Diff to &Local'), 'ldiff', 'Shift+Ctrl+D', _('View changes to current in external diff tool'), self.vdifflocal), ('edit', _('&View at Revision'), 'view-at-revision', 'Shift+Ctrl+E', _('View file as it appeared at this revision'), self.editfile), ('save', _('&Save at Revision...'), None, 'Shift+Ctrl+S', _('Save file as it appeared at this revision'), self.savefile), ('ledit', _('&Edit Local'), 'edit-file', None, _('Edit current file in working copy'), self.editlocal), ('lopen', _('&Open Local'), '', 'Shift+Ctrl+L', _('Edit current file in working copy'), self.openlocal), ('copypath', _('Copy &Path'), '', 'Shift+Ctrl+C', _('Copy full path of file(s) to the clipboard'), self.copypath), ('revert', _('&Revert to Revision...'), 'hg-revert', 'Shift+Ctrl+R', _('Revert file(s) to contents at this revision'), self.revertfile), ('opensubrepo', _('Open S&ubrepository'), 'thg-repository-open', None, _('Open the selected subrepository'), self.opensubrepo), ('explore', _('E&xplore Folder'), 'system-file-manager', None, _('Open the selected folder in the system file manager'), self.explore), ('terminal', _('Open &Terminal'), 'utilities-terminal', None, _('Open a shell terminal in the selected folder'), self.terminal), ]: act = QAction(desc, self) if icon: act.setIcon(qtlib.geticon(icon)) if key: act.setShortcut(key) if tip: act.setStatusTip(tip) if cb: act.triggered.connect(cb) self._actions[name] = act self._updateActions() def setRepo(self, repo): self.repo = repo def setRev(self, rev): self.ctx = self.repo[rev] self._updateActions() def _updateActions(self): rev = self.ctx.rev() real = type(rev) is int wd = rev is None for act in ['navigate', 'diffnavigate', 'ldiff', 'edit', 'save']: self._actions[act].setEnabled(real) for act in ['diff', 'revert']: self._actions[act].setEnabled(real or wd) def setPaths(self, selectedfiles, currentfile=None, itemissubrepo=False, itemisdir=False): """Set selected files [unicode]""" self.setPaths_(map(hglib.fromunicode, selectedfiles), hglib.fromunicode(currentfile), itemissubrepo, itemisdir) def setPaths_(self, selectedfiles, currentfile=None, itemissubrepo=False, itemisdir=False): """Set selected files [local encoding]""" if not currentfile and selectedfiles: currentfile = selectedfiles[0] self._selectedfiles = list(selectedfiles) self._currentfile = currentfile self._itemissubrepo = itemissubrepo self._itemisdir = itemisdir def actions(self): """List of the actions; The owner widget should register them""" return self._actions.values() def menu(self): """Menu for the current selection if available; otherwise None""" # Subrepos and regular items have different context menus if self._itemissubrepo: contextmenu = self._cachedcontextmenu('subrepo') elif self._itemisdir: contextmenu = self._cachedcontextmenu('dir') else: contextmenu = self._cachedcontextmenu('file') ln = len(self._selectedfiles) if ln == 0: return if ln > 1 and not self._itemissubrepo: singlefileactions = False else: singlefileactions = True self._actions['navigate'].setEnabled(singlefileactions) self._actions['diffnavigate'].setEnabled(singlefileactions) return contextmenu def _cachedcontextmenu(self, key): contextmenu = self._contextmenus.get(key) if contextmenu: return contextmenu contextmenu = QMenu(self.parent()) for act in _actionsbytype[key]: if act: contextmenu.addAction(self._actions[act]) else: contextmenu.addSeparator() self._setupCustomSubmenu(contextmenu) self._contextmenus[key] = contextmenu return contextmenu def _setupCustomSubmenu(self, menu): def make(text, func, types=None, icon=None, inmenu=None): action = inmenu.addAction(text) if icon: action.setIcon(qtlib.geticon(icon)) return action menu.addSeparator() customtools.addCustomToolsSubmenu(menu, self.repo.ui, location='workbench.filelist.custom-menu', make=make, slot=self._runCustomCommandByMenu) @pyqtSlot(QAction) def _runCustomCommandByMenu(self, action): files = [file for file in self._selectedfiles if os.path.exists(self.repo.wjoin(file))] if not files: qtlib.WarningMsgBox(_('File(s) not found'), _('The selected files do not exist in the working directory')) return self.runCustomCommandRequested.emit( str(action.data().toString()), files) def navigate(self): self._navigate(FileLogDialog) def diffNavigate(self): self._navigate(FileDiffDialog) def filterfile(self): """Ask to only show the revisions in which files on that folder are present""" if not self._selectedfiles: return self.filterRequested.emit("file('%s/**')" % self._selectedfiles[0]) def vdiff(self): repo, filenames, rev = self._findsub(self._selectedfiles) if not filenames: return if rev in repo.thgmqunappliedpatches: QMessageBox.warning(self.parent(), _("Cannot display visual diff"), _("Visual diffs are not supported for unapplied patches")) return opts = {'change': rev} dlg = visdiff.visualdiff(repo.ui, repo, filenames, opts) if dlg: dlg.exec_() def vdifflocal(self): repo, filenames, rev = self._findsub(self._selectedfiles) if not filenames: return assert type(rev) is int opts = {'rev': ['rev(%d)' % rev]} dlg = visdiff.visualdiff(repo.ui, repo, filenames, opts) if dlg: dlg.exec_() def editfile(self): repo, filenames, rev = self._findsub(self._selectedfiles) if not filenames: return if rev is None: qtlib.editfiles(repo, filenames, parent=self.parent()) else: base, _ = visdiff.snapshot(repo, filenames, repo[rev]) files = [os.path.join(base, filename) for filename in filenames] qtlib.editfiles(repo, files, parent=self.parent()) def savefile(self): repo, filenames, rev = self._findsub(self._selectedfiles) if not filenames: return qtlib.savefiles(repo, filenames, rev, parent=self.parent()) def editlocal(self): repo, filenames, _rev = self._findsub(self._selectedfiles) if not filenames: return qtlib.editfiles(repo, filenames, parent=self.parent()) def openlocal(self): repo, filenames, _rev = self._findsub(self._selectedfiles) if not filenames: return qtlib.openfiles(repo, filenames) def copypath(self): absfiles = [util.localpath(self.repo.wjoin(f)) for f in self._selectedfiles] QApplication.clipboard().setText( hglib.tounicode(os.linesep.join(absfiles))) def revertfile(self): repo, fileSelection, rev = self._findsub(self._selectedfiles) if not fileSelection: return if rev is None: rev = repo[rev].p1().rev() repoagent = repo._pyqtobj # TODO dlg = revert.RevertDialog(repoagent, fileSelection, rev, parent=self.parent()) dlg.exec_() def _navigate(self, dlgclass): repo, filename, rev = self._findsubsingle(self._currentfile) if filename and len(repo.file(filename)) > 0: repoagent = repo._pyqtobj # TODO dlg = self._nav_dialogs.open(dlgclass, repoagent, filename) dlg.goto(rev) def _createnavdialog(self, dlgclass, repoagent, filename): return dlgclass(repoagent, filename) def _gennavdialogkey(self, dlgclass, repoagent, filename): repo = repoagent.rawRepo() return dlgclass, repo.wjoin(filename) def _findsub(self, paths): """Find the nearest (sub-)repository for the given paths All paths should be in the same repository. Otherwise, unmatched paths are silently omitted. """ if not paths: return self.repo, [], self.ctx.rev() repopath, _relpath, ctx = hglib.getDeepestSubrepoContainingFile( paths[0], self.ctx) if not repopath: return self.repo, paths, self.ctx.rev() repo = thgrepo.repository(self.repo.ui, self.repo.wjoin(repopath)) pfx = repopath + '/' relpaths = [e[len(pfx):] for e in paths if e.startswith(pfx)] return repo, relpaths, ctx.rev() def _findsubsingle(self, path): if not path: return self.repo, None, self.ctx.rev() repo, relpaths, rev = self._findsub([path]) return repo, relpaths[0], rev def opensubrepo(self): path = os.path.join(self.repo.root, self._currentfile) spath = path[len(self.repo.root)+1:] if spath in self.ctx.substate and os.path.isdir(path): source, revid, stype = self.ctx.substate[spath] link = u'repo:' + hglib.tounicode(path) if stype == 'hg': link = u'%s?%s' % (link, revid) self.linkActivated.emit(link) else: QMessageBox.warning(self.parent(), _("Cannot open subrepository"), _("The selected subrepository does not exist on the working " "directory")) def explore(self): root = self.repo.wjoin(self._currentfile) if os.path.isdir(root): qtlib.openlocalurl(root) def terminal(self): root = self.repo.wjoin(self._currentfile) if os.path.isdir(root): qtlib.openshell(root, self._currentfile, self.repo.ui) tortoisehg-2.10/tortoisehg/hgqt/p4pending.py0000644000076400007640000001130312231647662020274 0ustar stevesteve# p4pending.py - Display pending p4 changelists, created by perfarce extension # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import error from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cslist, cmdcore, cmdui class PerforcePending(QDialog): 'Dialog for selecting a revision' showMessage = pyqtSignal(unicode) def __init__(self, repoagent, pending, url, parent): QDialog.__init__(self, parent) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self.url = url self.pending = pending # dict of changelist -> hash tuple layout = QVBoxLayout() self.setLayout(layout) clcombo = QComboBox() layout.addWidget(clcombo) self.cslist = cslist.ChangesetList(repo) layout.addWidget(self.cslist) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel|BB.Discard) bb.rejected.connect(self.reject) bb.button(BB.Discard).setText('Revert') bb.button(BB.Discard).setAutoDefault(False) bb.button(BB.Discard).clicked.connect(self.revert) bb.button(BB.Discard).setEnabled(False) bb.button(BB.Ok).setText('Submit') bb.button(BB.Ok).setAutoDefault(True) bb.button(BB.Ok).clicked.connect(self.submit) bb.button(BB.Ok).setEnabled(False) layout.addWidget(bb) self.bb = bb clcombo.activated[QString].connect(self.p4clActivated) for changelist in self.pending: clcombo.addItem(hglib.tounicode(changelist)) self.p4clActivated(clcombo.currentText()) self.setWindowTitle(_('Pending Perforce Changelists - %s') % repo.displayname) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) @pyqtSlot(QString) def p4clActivated(self, curcl): 'User has selected a changelist, fill cslist' repo = self._repoagent.rawRepo() curcl = hglib.fromunicode(curcl) try: hashes = self.pending[curcl] revs = [repo[hash] for hash in hashes] except (error.Abort, error.RepoLookupError), e: revs = [] self.cslist.clear() self.cslist.update(revs) sensitive = not curcl.endswith('(submitted)') self.bb.button(QDialogButtonBox.Ok).setEnabled(sensitive) self.bb.button(QDialogButtonBox.Discard).setEnabled(sensitive) self.curcl = curcl def submit(self): assert(self.curcl.endswith('(pending)')) cmdline = ['p4submit', '--verbose', '--config', 'extensions.perfarce=', '--repository', hglib.tounicode(self.url), hglib.tounicode(self.curcl[:-10])] self.bb.button(QDialogButtonBox.Ok).setEnabled(False) self.bb.button(QDialogButtonBox.Discard).setEnabled(False) self.showMessage.emit(_('Submitting p4 changelist...')) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self, worker='proc') sess.commandFinished.connect(self.commandFinished) def revert(self): assert(self.curcl.endswith('(pending)')) cmdline = ['p4revert', '--verbose', '--config', 'extensions.perfarce=', '--repository', hglib.tounicode(self.url), hglib.tounicode(self.curcl[:-10])] self.bb.button(QDialogButtonBox.Ok).setEnabled(False) self.bb.button(QDialogButtonBox.Discard).setEnabled(False) self.showMessage.emit(_('Reverting p4 changelist...')) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self, worker='proc') sess.commandFinished.connect(self.commandFinished) @pyqtSlot(int) def commandFinished(self, ret): self.showMessage.emit('') self.bb.button(QDialogButtonBox.Ok).setEnabled(True) self.bb.button(QDialogButtonBox.Discard).setEnabled(True) if ret == 0: self.reject() else: cmdui.errorMessageBox(self._cmdsession, self) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: if not self._cmdsession.isFinished(): self._cmdsession.abort() else: self.reject() else: return super(PerforcePending, self).keyPressEvent(event) tortoisehg-2.10/tortoisehg/hgqt/rejects.py0000644000076400007640000002400012231647662020041 0ustar stevesteve# rejects.py - TortoiseHg patch reject editor # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import cStringIO from mercurial import patch from hgext import record from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, qscilib, lexers from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4 import Qsci qsci = Qsci.QsciScintilla class RejectsDialog(QDialog): def __init__(self, ui, path, parent=None): super(RejectsDialog, self).__init__(parent) self.setWindowTitle(_('Merge rejected patch chunks into %s') % hglib.tounicode(path)) self.setWindowFlags(Qt.Window) self.path = path self.setLayout(QVBoxLayout()) editor = qscilib.Scintilla() editor.setBraceMatching(qsci.SloppyBraceMatch) editor.setFolding(qsci.BoxedTreeFoldStyle) editor.installEventFilter(qscilib.KeyPressInterceptor(self)) self.baseLineColor = editor.markerDefine(qsci.Background, -1) editor.setMarkerBackgroundColor(QColor('lightblue'), self.baseLineColor) self.layout().addWidget(editor, 3) searchbar = qscilib.SearchToolBar(self, hidable=True) searchbar.searchRequested.connect(editor.find) searchbar.conditionChanged.connect(editor.highlightText) searchbar.hide() def showsearchbar(): searchbar.show() searchbar.setFocus(Qt.OtherFocusReason) qtlib.newshortcutsforstdkey(QKeySequence.Find, self, showsearchbar) self.layout().addWidget(searchbar) hbox = QHBoxLayout() hbox.setContentsMargins(2, 2, 2, 2) self.layout().addLayout(hbox, 1) self.chunklist = QListWidget(self) self.updating = True self.chunklist.currentRowChanged.connect(self.showChunk) hbox.addWidget(self.chunklist, 1) bvbox = QVBoxLayout() bvbox.setContentsMargins(2, 2, 2, 2) self.resolved = tb = QToolButton() tb.setIcon(qtlib.geticon('thg-success')) tb.setToolTip(_('Mark this chunk as resolved, goto next unresolved')) tb.pressed.connect(self.resolveCurrentChunk) self.unresolved = tb = QToolButton() tb.setIcon(qtlib.geticon('thg-warning')) tb.setToolTip(_('Mark this chunk as unresolved')) tb.pressed.connect(self.unresolveCurrentChunk) bvbox.addStretch(1) bvbox.addWidget(self.resolved, 0) bvbox.addWidget(self.unresolved, 0) bvbox.addStretch(1) hbox.addLayout(bvbox, 0) self.editor = editor self.rejectbrowser = RejectBrowser(self) hbox.addWidget(self.rejectbrowser, 5) BB = QDialogButtonBox bb = QDialogButtonBox(BB.Save|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.layout().addWidget(bb) self.saveButton = bb.button(BB.Save) s = QSettings() self.restoreGeometry(s.value('rejects/geometry').toByteArray()) self.editor.loadSettings(s, 'rejects/editor') self.rejectbrowser.loadSettings(s, 'rejects/rejbrowse') f = QFile(hglib.tounicode(path)) if not f.open(QIODevice.ReadOnly): qtlib.ErrorMsgBox(_('Unable to merge rejects'), _("Can't read this file (maybe deleted)")) self.hide() QTimer.singleShot(0, self.reject) return earlybytes = f.read(4096) if '\0' in earlybytes: qtlib.ErrorMsgBox(_('Unable to merge rejects'), _('This appears to be a binary file')) self.hide() QTimer.singleShot(0, self.reject) return f.seek(0) editor.read(f) editor.setModified(False) lexer = lexers.getlexer(ui, path, earlybytes, self) editor.setLexer(lexer) editor.setMarginLineNumbers(1, True) editor.setMarginWidth(1, str(editor.lines())+'X') buf = cStringIO.StringIO() try: buf.write('diff -r aaaaaaaaaaaa -r bbbbbbbbbbb %s\n' % path) buf.write(open(path + '.rej', 'rb').read()) buf.seek(0) except IOError, e: pass try: header = record.parsepatch(buf)[0] self.chunks = header.hunks except (patch.PatchError, IndexError), e: self.chunks = [] for chunk in self.chunks: chunk.resolved = False self.updateChunkList() self.saveButton.setDisabled(len(self.chunks)) self.resolved.setDisabled(True) self.unresolved.setDisabled(True) QTimer.singleShot(0, lambda: self.chunklist.setCurrentRow(0)) def updateChunkList(self): self.updating = True self.chunklist.clear() for chunk in self.chunks: self.chunklist.addItem('@@ %d %s' % (chunk.fromline, chunk.resolved and '(resolved)' or '(unresolved)')) self.updating = False @pyqtSlot() def resolveCurrentChunk(self): row = self.chunklist.currentRow() chunk = self.chunks[row] chunk.resolved = True self.updateChunkList() for i, chunk in enumerate(self.chunks): if not chunk.resolved: self.chunklist.setCurrentRow(i) return else: self.chunklist.setCurrentRow(row) self.saveButton.setEnabled(True) @pyqtSlot() def unresolveCurrentChunk(self): row = self.chunklist.currentRow() chunk = self.chunks[row] chunk.resolved = False self.updateChunkList() self.chunklist.setCurrentRow(row) self.saveButton.setEnabled(False) @pyqtSlot(int) def showChunk(self, row): if row == -1 or self.updating: return buf = cStringIO.StringIO() chunk = self.chunks[row] chunk.write(buf) startline = max(chunk.fromline-1, 0) self.rejectbrowser.showChunk(buf.getvalue().splitlines(True)[1:]) self.editor.setCursorPosition(startline, 0) self.editor.ensureLineVisible(startline) self.editor.markerDeleteAll(-1) self.editor.markerAdd(startline, self.baseLineColor) self.resolved.setEnabled(not chunk.resolved) self.unresolved.setEnabled(chunk.resolved) def saveSettings(self): s = QSettings() s.setValue('rejects/geometry', self.saveGeometry()) self.editor.saveSettings(s, 'rejects/editor') self.rejectbrowser.saveSettings(s, 'rejects/rejbrowse') def accept(self): # If the editor has been modified, we implicitly accept the changes acceptresolution = self.editor.isModified() if not acceptresolution: action = QMessageBox.warning(self, _("Warning"), _("You have marked all rejected patch chunks as resolved yet you " \ "have not modified the file on the edit panel.\n\n" \ "This probably means that no code from any of the rejected patch " \ "chunks made it into the file.\n\n"\ "Are you sure that you want to leave the file as is and " \ "consider all the rejected patch chunks as resolved?\n\n" \ "Doing so may delete them from a shelve, for example, which " \ "would mean that you would lose them forever!\n\n" "Click Yes to accept the file as is or No to continue resolving " \ "the rejected patch chunks."), QMessageBox.Yes, QMessageBox.No) if action == QMessageBox.Yes: acceptresolution = True if acceptresolution: f = QFile(hglib.tounicode(self.path)) saved = f.open(QIODevice.WriteOnly) and self.editor.write(f) if not saved: qtlib.ErrorMsgBox(_('Unable to save file'), f.errorString(), parent=self) return self.saveSettings() super(RejectsDialog, self).accept() def reject(self): self.saveSettings() super(RejectsDialog, self).reject() class RejectBrowser(qscilib.Scintilla): 'Display a rejected diff hunk in an easily copy/pasted format' def __init__(self, parent): super(RejectBrowser, self).__init__(parent) self.setFrameStyle(0) self.setReadOnly(True) self.setUtf8(True) self.installEventFilter(qscilib.KeyPressInterceptor(self)) self.setCaretLineVisible(False) self.setMarginType(1, qsci.SymbolMargin) self.setMarginLineNumbers(1, False) self.setMarginWidth(1, QFontMetrics(self.font()).width('XX')) self.setMarginSensitivity(1, True) self.addedMark = self.markerDefine(qsci.Plus, -1) self.removedMark = self.markerDefine(qsci.Minus, -1) self.addedColor = self.markerDefine(qsci.Background, -1) self.removedColor = self.markerDefine(qsci.Background, -1) self.setMarkerBackgroundColor(QColor('lightgreen'), self.addedColor) self.setMarkerBackgroundColor(QColor('cyan'), self.removedColor) mask = (1 << self.addedMark) | (1 << self.removedMark) | \ (1 << self.addedColor) | (1 << self.removedColor) self.setMarginMarkerMask(1, mask) lexer = lexers.difflexer(self) self.setLexer(lexer) def showChunk(self, lines): utext = [] added = [] removed = [] for i, line in enumerate(lines): utext.append(hglib.tounicode(line[1:])) if line[0] == '+': added.append(i) elif line[0] == '-': removed.append(i) self.markerDeleteAll(-1) self.setText(u''.join(utext)) for i in added: self.markerAdd(i, self.addedMark) self.markerAdd(i, self.addedColor) for i in removed: self.markerAdd(i, self.removedMark) self.markerAdd(i, self.removedColor) tortoisehg-2.10/tortoisehg/hgqt/merge.py0000644000076400007640000006164712231647662017523 0ustar stevesteve# merge.py - Merge dialog for TortoiseHg # # Copyright 2010 Yuki KODAMA # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from mercurial import error, util from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib, csinfo, i18n, cmdui, status, resolve from tortoisehg.hgqt import qscilib, thgrepo, messageentry, commit, wctxcleaner from PyQt4.QtCore import * from PyQt4.QtGui import * MARGINS = (8, 0, 0, 0) class MergeDialog(QWizard): def __init__(self, repoagent, otherrev, parent=None): super(MergeDialog, self).__init__(parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) repo = repoagent.rawRepo() self.otherrev = str(otherrev) self.localrev = str(repo['.'].rev()) self.setWindowTitle(_('Merge - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-merge')) self.setOption(QWizard.NoBackButtonOnStartPage, True) self.setOption(QWizard.NoBackButtonOnLastPage, True) self.setOption(QWizard.IndependentPages, True) # set pages summarypage = SummaryPage(repoagent, self) self.addPage(summarypage) self.addPage(MergePage(repoagent, self)) self.addPage(CommitPage(repoagent, self)) self.addPage(ResultPage(repoagent, self)) self.currentIdChanged.connect(self.pageChanged) # move focus to "Next" button so that "Cancel" doesn't eat Enter key summarypage.refreshFinished.connect( self.button(QWizard.NextButton).setFocus) self.resize(QSize(700, 489).expandedTo(self.minimumSizeHint())) repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.configChanged.connect(self.configChanged) @pyqtSlot() def repositoryChanged(self): if self.currentPage(): self.currentPage().repositoryChanged() @pyqtSlot() def configChanged(self): if self.currentPage(): self.currentPage().configChanged() def pageChanged(self, id): if id != -1: self.currentPage().currentPage() def reject(self): if self.currentPage().canExit(): super(MergeDialog, self).reject() class BasePage(QWizardPage): def __init__(self, repoagent, parent): super(BasePage, self).__init__(parent) self._repoagent = repoagent @property def repo(self): return self._repoagent.rawRepo() def validatePage(self): 'user pressed NEXT button, can we proceed?' return True def isComplete(self): 'should NEXT button be sensitive?' return True def repositoryChanged(self): 'repository has detected a change to changelog or parents' pass def configChanged(self): 'repository has detected a change to config files' pass def currentPage(self): self.wizard().setOption(QWizard.NoDefaultButton, False) def canExit(self): if len(self.repo.parents()) == 2: main = _('Do you want to exit?') text = _('To finish merging, you must commit ' 'the working directory.\n\n' 'To cancel the merge you can update to one ' 'of the merge parent revisions.') labels = ((QMessageBox.Yes, _('&Exit')), (QMessageBox.No, _('Cancel'))) if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, labels=labels, parent=self): return False return True class SummaryPage(BasePage): refreshFinished = pyqtSignal() def __init__(self, repoagent, parent): super(SummaryPage, self).__init__(repoagent, parent) self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkStarted.connect(self._onCheckStarted) self._wctxcleaner.checkFinished.connect(self._onCheckFinished) ### Override Methods ### def initializePage(self): if self.layout(): return self.setTitle(_('Prepare to merge')) self.setSubTitle(_('Verify merge targets and ensure your working ' 'directory is clean.')) self.setLayout(QVBoxLayout()) repo = self.repo contents = ('ishead',) + csinfo.PANEL_DEFAULT style = csinfo.panelstyle(contents=contents) def markup_func(widget, item, value): if item == 'ishead' and value is False: text = _('Not a head revision!') return qtlib.markup(text, fg='red', weight='bold') raise csinfo.UnknownItem(item) custom = csinfo.custom(markup=markup_func) create = csinfo.factory(repo, custom, style, withupdate=True) ## merge target other_sep = qtlib.LabeledSeparator(_('Merge from (other revision)')) self.layout().addWidget(other_sep) try: otherCsInfo = create(self.wizard().otherrev) self.layout().addWidget(otherCsInfo) self.otherCsInfo = otherCsInfo except error.RepoLookupError: qtlib.InfoMsgBox(_('Unable to merge'), _('Merge revision not specified or not found')) QTimer.singleShot(0, self.wizard().close) ## current revision local_sep = qtlib.LabeledSeparator(_('Merge to (working directory)')) self.layout().addWidget(local_sep) localCsInfo = create(self.wizard().localrev) self.layout().addWidget(localCsInfo) self.localCsInfo = localCsInfo ## working directory status wd_sep = qtlib.LabeledSeparator(_('Working directory status')) self.layout().addWidget(wd_sep) self.groups = qtlib.WidgetGroups() wdbox = QHBoxLayout() self.layout().addLayout(wdbox) self.wd_status = qtlib.StatusLabel() self.wd_status.set_status(_('Checking...')) wdbox.addWidget(self.wd_status) wd_prog = QProgressBar() wd_prog.setMaximum(0) wd_prog.setTextVisible(False) self.groups.add(wd_prog, 'prog') wdbox.addWidget(wd_prog, 1) wd_merged = QLabel(_('The working directory is already merged. ' 'Continue or ' 'discard existing ' 'merge.')) wd_merged.linkActivated.connect(self.onLinkActivated) wd_merged.setWordWrap(True) self.groups.add(wd_merged, 'merged') self.layout().addWidget(wd_merged) text = _('Before merging, you must commit, ' 'shelve to patch, ' 'or discard changes.') wd_text = QLabel(text) wd_text.setWordWrap(True) wd_text.linkActivated.connect(self._wctxcleaner.runCleaner) self.wd_text = wd_text self.groups.add(wd_text, 'dirty') self.layout().addWidget(wd_text) wdbox = QHBoxLayout() self.layout().addLayout(wdbox) wd_alt = QLabel(_('Or use:')) self.groups.add(wd_alt, 'dirty') wdbox.addWidget(wd_alt) force_chk = QCheckBox(_('Force a merge with outstanding changes ' '(-f/--force)')) force_chk.toggled.connect(lambda c: self.completeChanged.emit()) self.registerField('force', force_chk) self.groups.add(force_chk, 'dirty') wdbox.addWidget(force_chk) ### options expander = qtlib.ExpanderLabel(_('Options'), False) expander.expanded.connect(self.toggleShowOptions) self.layout().addWidget(expander) self.expander = expander ### discard option discard_chk = QCheckBox(_('Discard all changes from merge target ' '(other) revision')) self.registerField('discard', discard_chk) self.layout().addWidget(discard_chk) self.discard_chk = discard_chk ## auto-resolve autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) autoresolve_chk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', False)) self.registerField('autoresolve', autoresolve_chk) self.layout().addWidget(autoresolve_chk) self.autoresolve_chk = autoresolve_chk self.groups.set_visible(False, 'dirty') self.groups.set_visible(False, 'merged') self.toggleShowOptions(self.expander.is_expanded()) def isComplete(self): 'should Next button be sensitive?' return self._wctxcleaner.isClean() or self.field('force').toBool() def validatePage(self): 'validate that we can continue with the merge' if self.field('discard').toBool(): labels = [(QMessageBox.Yes, _('&Discard')), (QMessageBox.No, _('Cancel'))] if not qtlib.QuestionMsgBox(_('Confirm Discard Changes'), _('The changes from revision %s and all unmerged parents ' 'will be discarded.\n\n' 'Are you sure this is what you want to do?') % (self.otherCsInfo.get_data('revid')), labels=labels, parent=self): return False return super(SummaryPage, self).validatePage(); ## custom methods ## def toggleShowOptions(self, visible): self.discard_chk.setShown(visible) self.autoresolve_chk.setShown(visible) def repositoryChanged(self): 'repository has detected a change to changelog or parents' pctx = self.repo['.'] self.localCsInfo.update(pctx) self.wizard().localrev = str(pctx.rev()) def canExit(self): 'can merge tool be closed?' if self._wctxcleaner.isChecking(): self._wctxcleaner.cancelCheck() return True def currentPage(self): super(SummaryPage, self).currentPage() self.refresh() def refresh(self): self._wctxcleaner.check() @pyqtSlot() def _onCheckStarted(self): self.groups.set_visible(True, 'prog') @pyqtSlot(bool, int) def _onCheckFinished(self, clean, parents): self.groups.set_visible(False, 'prog') if self._wctxcleaner.isCheckCanceled(): return if not clean: self.groups.set_visible(parents == 2, 'merged') self.groups.set_visible(parents == 1, 'dirty') self.wd_status.set_status(_('Uncommitted local changes ' 'are detected'), 'thg-warning') else: self.groups.set_visible(False, 'dirty') self.groups.set_visible(False, 'merged') self.wd_status.set_status(_('Clean', 'working dir state'), True) self.completeChanged.emit() self.refreshFinished.emit() @pyqtSlot(QString) def onLinkActivated(self, cmd): if cmd == 'skip': self.wizard().next() else: self._wctxcleaner.runCleaner(cmd) class MergePage(BasePage): def __init__(self, repoagent, parent): super(MergePage, self).__init__(repoagent, parent) self.mergecomplete = False self.setTitle(_('Merging...')) self.setSubTitle(_('All conflicting files will be marked unresolved.')) self.setLayout(QVBoxLayout()) self.cmd = cmdui.Widget(True, False, self) self.cmd.commandFinished.connect(self.onCommandFinished) self.cmd.setShowOutput(True) self.layout().addWidget(self.cmd) self.reslabel = QLabel() self.reslabel.linkActivated.connect(self.onLinkActivated) self.reslabel.setWordWrap(True) self.layout().addWidget(self.reslabel) self.autonext = QCheckBox(_('Automatically advance to next page ' 'when merge is complete.')) checked = QSettings().value('merge/autoadvance', False).toBool() self.autonext.setChecked(checked) self.autonext.toggled.connect(self.tryAutoAdvance) self.layout().addWidget(self.autonext) def currentPage(self): super(MergePage, self).currentPage() if self.field('discard').toBool(): # '.' is safer than self.localrev, in case the user has # pulled a fast one on us and updated from the CLI cmdline = ['--repository', self.repo.root, 'debugsetparents', '.', self.wizard().otherrev] else: cmdline = ['--repository', self.repo.root, 'merge', '--verbose'] if self.field('force').toBool(): cmdline.append('--force') tool = self.field('autoresolve').toBool() and 'merge' or 'fail' cmdline += ['--tool=internal:' + tool] cmdline.append(self.wizard().otherrev) if len(self.repo.parents()) == 1: self.repo.incrementBusyCount() self.cmd.core.clearOutput() self.cmd.run(cmdline) else: self.mergecomplete = True self.completeChanged.emit() def isComplete(self): 'should Next button be sensitive?' if not self.mergecomplete: return False count = 0 for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': count += 1 if count: if self.field('autoresolve').toBool(): # if autoresolve is enabled, we know these were real conflicts self.reslabel.setText(_('%d files have merge conflicts ' 'that must be ' 'resolved') % count) else: # else give a calmer indication of conflicts self.reslabel.setText(_('%d files were modified on both ' 'branches and must be ' 'resolved') % count) return False else: self.reslabel.setText(_('No merge conflicts, ready to commit')) return True def tryAutoAdvance(self, checked): if checked and self.isComplete(): self.wizard().next() def cleanupPage(self): QSettings().setValue('merge/autoadvance', self.autonext.isChecked()) def onCommandFinished(self, ret): self.repo.decrementBusyCount() if ret in (0, 1): self.mergecomplete = True if self.autonext.isChecked(): self.tryAutoAdvance(True) self.completeChanged.emit() @pyqtSlot(QString) def onLinkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() if self.autonext.isChecked(): self.tryAutoAdvance(True) self.completeChanged.emit() class CommitPage(BasePage): def __init__(self, repoagent, parent): super(CommitPage, self).__init__(repoagent, parent) self.setTitle(_('Commit merge results')) self.setSubTitle(' ') self.setLayout(QVBoxLayout()) self.setCommitPage(True) repo = repoagent.rawRepo() # csinfo def label_func(widget, item, ctx): if item == 'rev': return _('Revision:') elif item == 'parents': return _('Parents') raise csinfo.UnknownItem() def data_func(widget, item, ctx): if item == 'rev': return _('Working Directory'), str(ctx) elif item == 'parents': parents = [] cbranch = ctx.branch() for pctx in ctx.parents(): branch = None if hasattr(pctx, 'branch') and pctx.branch() != cbranch: branch = pctx.branch() parents.append((str(pctx.rev()), str(pctx), branch, pctx)) return parents raise csinfo.UnknownItem() def markup_func(widget, item, value): if item == 'rev': text, rev = value return '%s (%s)' % (text, rev) elif item == 'parents': def branch_markup(branch): opts = dict(fg='black', bg='#aaffaa') return qtlib.markup(' %s ' % branch, **opts) csets = [] for rnum, rid, branch, pctx in value: line = '%s (%s)' % (rnum, rid) if branch: line = '%s %s' % (line, branch_markup(branch)) msg = widget.info.get_data('summary', widget, pctx, widget.custom) if msg: line = '%s %s' % (line, msg) csets.append(line) return csets raise csinfo.UnknownItem() custom = csinfo.custom(label=label_func, data=data_func, markup=markup_func) contents = ('rev', 'user', 'dateage', 'branch', 'parents') style = csinfo.panelstyle(contents=contents, margin=6) # merged files rev_sep = qtlib.LabeledSeparator(_('Working Directory (merged)')) self.layout().addWidget(rev_sep) mergeCsInfo = csinfo.create(repo, None, style, custom=custom, withupdate=True) mergeCsInfo.linkActivated.connect(self.onLinkActivated) self.layout().addWidget(mergeCsInfo) self.mergeCsInfo = mergeCsInfo # commit message area msg_sep = qtlib.LabeledSeparator(_('Commit message')) self.layout().addWidget(msg_sep) msgEntry = messageentry.MessageEntry(self) msgEntry.installEventFilter(qscilib.KeyPressInterceptor(self)) msgEntry.refresh(repo) msgEntry.loadSettings(QSettings(), 'merge/message') msgEntry.textChanged.connect(self.completeChanged) self.layout().addWidget(msgEntry) self.msgEntry = msgEntry self.cmd = cmdui.Widget(True, False, self) self.cmd.commandFinished.connect(self.onCommandFinished) self.cmd.setShowOutput(False) self.layout().addWidget(self.cmd) self.delayednext = False def tryperform(): if self.isComplete(): self.wizard().next() actionEnter = QAction('alt-enter', self) actionEnter.setShortcuts([Qt.CTRL+Qt.Key_Return, Qt.CTRL+Qt.Key_Enter]) actionEnter.triggered.connect(tryperform) self.addAction(actionEnter) self.skiplast = QCheckBox(_('Skip final confirmation page, ' 'close after commit.')) checked = QSettings().value('merge/skiplast', False).toBool() self.skiplast.setChecked(checked) self.layout().addWidget(self.skiplast) hblayout = QHBoxLayout() self.opts = commit.readopts(self.repo.ui) self.optionsbtn = QPushButton(_('Commit Options')) self.optionsbtn.clicked.connect(self.details) hblayout.addWidget(self.optionsbtn) self.optionslabelfmt = _('Selected Options: %s') self.optionslabel = QLabel('') hblayout.addWidget(self.optionslabel) hblayout.addStretch() self.layout().addLayout(hblayout) self.setButtonText(QWizard.CommitButton, _('Commit Now')) # The cancel button does not really "cancel" the merge self.setButtonText(QWizard.CancelButton, _('Commit Later')) # Update the options label self.refresh() def refresh(self): opts = commit.commitopts2str(self.opts) self.optionslabel.setText(self.optionslabelfmt % hglib.tounicode(opts)) self.optionslabel.setVisible(bool(opts)) def cleanupPage(self): s = QSettings() s.setValue('merge/skiplast', self.skiplast.isChecked()) self.msgEntry.saveSettings(s, 'merge/message') def currentPage(self): super(CommitPage, self).currentPage() self.wizard().setOption(QWizard.NoDefaultButton, True) self.mergeCsInfo.update() # show post-merge state engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False) wctx = self.repo[None] if wctx.p1().branch() == wctx.p2().branch(): msgset = i18n.keepgettext()._('Merge') text = engmsg and msgset['id'] or msgset['str'] text = unicode(text) else: msgset = i18n.keepgettext()._('Merge with %s') text = engmsg and msgset['id'] or msgset['str'] text = unicode(text) % hglib.tounicode(wctx.p2().branch()) self.msgEntry.setText(text) self.msgEntry.moveCursorToEnd() @pyqtSlot(QString) def onLinkActivated(self, cmd): if cmd == 'view': dlg = status.StatusDialog(self._repoagent, [], {}, self) dlg.exec_() self.refresh() def isComplete(self): return len(self.repo.parents()) == 2 and len(self.msgEntry.text()) > 0 def validatePage(self): if self.cmd.core.running(): return False if len(self.repo.parents()) == 1: # commit succeeded, repositoryChanged() called wizard().next() if self.skiplast.isChecked(): self.wizard().close() return True user = qtlib.getCurrentUsername(self, self.repo, self.opts) if not user: return False self.setTitle(_('Committing...')) self.setSubTitle(_('Please wait while committing merged files.')) message = hglib.fromunicode(self.msgEntry.text()) cmdline = ['commit', '--verbose', '--message', message, '--repository', self.repo.root, '--user', user] if self.opts.get('recurseinsubrepos'): cmdline.append('--subrepos') try: date = self.opts.get('date') if date: util.parsedate(date) dcmd = ['--date', date] else: dcmd = [] except error.Abort, e: if e.hint: err = _('%s (hint: %s)') % (hglib.tounicode(str(e)), hglib.tounicode(e.hint)) else: err = hglib.tounicode(str(e)) qtlib.WarningMsgBox(_('TortoiseHg Merge Commit'), _('Error creating interpreting commit date (%s).\n' 'Using current date instead.'), err) dcmd = [] cmdline += dcmd commandlines = [cmdline] pushafter = self.repo.ui.config('tortoisehg', 'cipushafter') if pushafter: cmd = ['push', '--repository', self.repo.root, pushafter] commandlines.append(cmd) self.repo.incrementBusyCount() self.cmd.setShowOutput(True) self.cmd.run(*commandlines) return False def repositoryChanged(self): 'repository has detected a change to changelog or parents' if len(self.repo.parents()) == 1: if self.cmd.core.running(): # call self.wizard().next() after the current command finishes self.delayednext = True else: self.wizard().next() def onCommandFinished(self, ret): self.repo.decrementBusyCount() if self.delayednext: self.delayednext = False self.wizard().next() self.completeChanged.emit() def readUserHistory(self): 'Load user history from the global commit settings' s = QSettings() userhist = s.value('commit/userhist').toStringList() userhist = [u for u in userhist if u] return userhist def details(self): self.userhist = self.readUserHistory() dlg = commit.DetailsDialog(self.opts, self.userhist, self, mode='merge') dlg.finished.connect(dlg.deleteLater) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) if dlg.exec_() == QDialog.Accepted: self.opts.update(dlg.outopts) self.refresh() class ResultPage(BasePage): def __init__(self, repoagent, parent): super(ResultPage, self).__init__(repoagent, parent) self.setTitle(_('Finished')) self.setSubTitle(' ') self.setFinalPage(True) self.setLayout(QVBoxLayout()) merge_sep = qtlib.LabeledSeparator(_('Merge changeset')) self.layout().addWidget(merge_sep) mergeCsInfo = csinfo.create(self.repo, 'tip', withupdate=True) self.layout().addWidget(mergeCsInfo) self.mergeCsInfo = mergeCsInfo self.layout().addStretch(1) def currentPage(self): super(ResultPage, self).currentPage() self.mergeCsInfo.update(self.repo['tip']) self.wizard().setOption(QWizard.NoCancelButton, True) tortoisehg-2.10/tortoisehg/hgqt/partialcommit.py0000644000076400007640000000643312110205646021245 0ustar stevesteve# partialcommit.py - commit extension for partial commits (change selection) # # Copyright 2012 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import patch, commands, extensions, context, util, node def partialcommit(orig, ui, repo, *pats, **opts): patchfilename = opts.get('partials', None) if patchfilename: # attach a patch.filestore to this repo prior to calling commit() # the wrapped workingfilectx methods will see this filestore and use # the patched file data rather than the working copy data (for only the # files modified by the patch) fp = open(patchfilename, 'rb') store = patch.filestore() try: try: # patch files in tmp directory patch.patchrepo(ui, repo, repo['.'], store, fp, 1, None) store.keys = set(store.files.keys() + store.data.keys()) repo._filestore = store except patch.PatchError, e: raise util.Abort(str(e)) finally: fp.close() try: ret = orig(ui, repo, *pats, **opts) if hasattr(repo, '_filestore'): store.close() wlock = repo.wlock() try: # mark partially committed files for 'needing lookup' in # the dirstate. The next status call will find them as M for f in store.keys: repo.dirstate.normallookup(f) finally: wlock.release() return ret finally: if patchfilename: os.unlink(patchfilename) def wfctx_data(orig, self): 'wrapper function for workingfilectx.data()' if hasattr(self._repo, '_filestore'): store = self._repo._filestore if self._path in store.keys: data, (islink, isexec), copied = store.getfile(self._path) return data return orig(self) def wfctx_flags(orig, self): 'wrapper function for workingfilectx.flags()' if hasattr(self._repo, '_filestore'): store = self._repo._filestore if self._path in store.keys: data, (islink, isexec), copied = store.getfile(self._path) return (islink and 'l' or '') + (isexec and 'x' or '') return orig(self) def wfctx_renamed(orig, self): 'wrapper function for workingfilectx.renamed()' if hasattr(self._repo, '_filestore'): store = self._repo._filestore if self._path in store.keys: data, (islink, isexec), copied = store.getfile(self._path) if copied: return copied, node.nullid else: return None return orig(self) registered = False def uisetup(ui): global registered if registered: return registered = True extensions.wrapfunction(context.workingfilectx, 'data', wfctx_data) extensions.wrapfunction(context.workingfilectx, 'flags', wfctx_flags) extensions.wrapfunction(context.workingfilectx, 'renamed', wfctx_renamed) entry = extensions.wrapcommand(commands.table, 'commit', partialcommit) entry[1].append(('', 'partials', '', 'selected patch chunks (internal use only)')) tortoisehg-2.10/tortoisehg/hgqt/postreview.py0000644000076400007640000003457312231647662020631 0ustar stevesteve# postreview.py - post review dialog for TortoiseHg # # Copyright 2011 Michael De Wildt # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. """A dialog to allow users to post a review to reviewboard http:///www.reviewboard.org This dialog requires a fork of the review board mercurial plugin, maintained by mdelagra, that can be downloaded from: https://bitbucket.org/mdelagra/mercurial-reviewboard/ More information can be found at http://www.mikeyd.com.au/tortoisehg-reviewboard """ from PyQt4.QtCore import * from PyQt4.QtGui import * from mercurial import extensions, scmutil from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import cmdcore, qtlib from tortoisehg.hgqt.postreview_ui import Ui_PostReviewDialog from tortoisehg.hgqt.hgemail import _ChangesetsModel class LoadReviewDataThread(QThread): def __init__ (self, dialog): super(LoadReviewDataThread, self).__init__(dialog) self.dialog = dialog def run(self): msg = None if not self.dialog.server: msg = _("Invalid Settings - The ReviewBoard server is not setup") elif not self.dialog.user: msg = _("Invalid Settings - Please provide your ReviewBoard username") else: rb = extensions.find("reviewboard") try: pwd = self.dialog.password #if we don't have a password send something here to skip #the cli getpass in the extension. We will set the password #later if not pwd: pwd = "None" self.reviewboard = rb.make_rbclient(self.dialog.server, self.dialog.user, pwd) self.loadCombos() except rb.ReviewBoardError, e: msg = e.msg except TypeError: msg = _("Invalid reviewboard plugin. Please download the " "Mercurial reviewboard plugin version 3.5 or higher " "from the website below.\n\n %s") % \ u'http://bitbucket.org/mdelagra/mercurial-reviewboard/' self.dialog.error_message = msg def loadCombos(self): #Get the index of a users previously selected repo id index = 0 count = 0 self.dialog.qui.progress_label.setText("Loading repositories...") for r in self.reviewboard.repositories(): if r.id == self.dialog.repo_id: index = count self.dialog.qui.repo_id_combo.addItem(str(r.id) + ": " + r.name) count += 1 if self.dialog.qui.repo_id_combo.count(): self.dialog.qui.repo_id_combo.setCurrentIndex(index) self.dialog.qui.progress_label.setText("Loading existing reviews...") for r in self.reviewboard.pending_user_requests(): summary = str(r.id) + ": " + r.summary[0:100] self.dialog.qui.review_id_combo.addItem(summary) if self.dialog.qui.review_id_combo.count(): self.dialog.qui.review_id_combo.setCurrentIndex(0) class PostReviewDialog(QDialog): """Dialog for sending patches to reviewboard""" def __init__(self, ui, repoagent, revs, parent=None): super(PostReviewDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.ui = ui self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._cmdoutputs = [] self.error_message = None self.qui = Ui_PostReviewDialog() self.qui.setupUi(self) self.initChangesets(revs) self.readSettings() self.review_thread = LoadReviewDataThread(self) self.review_thread.finished.connect(self.errorPrompt) self.review_thread.start() QShortcut(QKeySequence('Ctrl+Return'), self, self.accept) QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def passwordPrompt(self): pwd, ok = qtlib.getTextInput(self, _('Review Board'), _('Password:'), mode=QLineEdit.Password) if ok and pwd: self.password = pwd return True else: self.password = None return False @pyqtSlot() def errorPrompt(self): self.qui.progress_bar.hide() self.qui.progress_label.hide() if self.error_message: qtlib.ErrorMsgBox(_('Review Board'), _('Error'), self.error_message) self.close() elif self.isValid(): self.qui.post_review_button.setEnabled(True) def closeEvent(self, event): if not self._cmdsession.isFinished(): self._cmdsession.abort() event.ignore() return # Dispose of the review data thread self.review_thread.terminate() self.review_thread.wait() self.writeSettings() super(PostReviewDialog, self).closeEvent(event) def readSettings(self): s = QSettings() self.restoreGeometry(s.value('reviewboard/geom').toByteArray()) self.qui.publish_immediately_check.setChecked( s.value('reviewboard/publish_immediately_check').toBool()) self.qui.outgoing_changes_check.setChecked( s.value('reviewboard/outgoing_changes_check').toBool()) self.qui.branch_check.setChecked( s.value('reviewboard/branch_check').toBool()) self.qui.update_fields.setChecked( s.value('reviewboard/update_fields').toBool()) self.qui.summary_edit.addItems( s.value('reviewboard/summary_edit_history').toStringList()) try: self.repo_id = int(self.repo.ui.config('reviewboard', 'repoid')) except Exception: self.repo_id = None if not self.repo_id: self.repo_id = s.value('reviewboard/repo_id').toInt()[0] self.server = self.repo.ui.config('reviewboard', 'server') self.user = self.repo.ui.config('reviewboard', 'user') self.password = self.repo.ui.config('reviewboard', 'password') self.browser = self.repo.ui.config('reviewboard', 'browser') def writeSettings(self): s = QSettings() s.setValue('reviewboard/geom', self.saveGeometry()) s.setValue('reviewboard/publish_immediately_check', self.qui.publish_immediately_check.isChecked()) s.setValue('reviewboard/branch_check', self.qui.branch_check.isChecked()) s.setValue('reviewboard/outgoing_changes_check', self.qui.outgoing_changes_check.isChecked()) s.setValue('reviewboard/update_fields', self.qui.update_fields.isChecked()) s.setValue('reviewboard/repo_id', self.getRepoId()) def itercombo(w): if w.currentText(): yield w.currentText() for i in xrange(w.count()): if w.itemText(i) != w.currentText(): yield w.itemText(i) s.setValue('reviewboard/summary_edit_history', list(itercombo(self.qui.summary_edit))[:10]) def initChangesets(self, revs, selected_revs=None): def purerevs(revs): return scmutil.revrange(self.repo, iter(str(e) for e in revs)) if selected_revs: selectedrevs = purerevs(selected_revs) else: selectedrevs = purerevs(revs) self._changesets = _ChangesetsModel(self.repo, # TODO: [':'] is inefficient revs=purerevs(revs or [':']), selectedrevs=selectedrevs, parent=self) self.qui.changesets_view.setModel(self._changesets) @property def selectedRevs(self): """Returns list of revisions to be sent""" return self._changesets.selectedrevs @property def allRevs(self): """Returns list of revisions to be sent""" return self._changesets.revs def getRepoId(self): comboText = self.qui.repo_id_combo.currentText().split(":") return str(comboText[0]) def getReviewId(self): comboText = self.qui.review_id_combo.currentText().split(":") return str(comboText[0]) def getSummary(self): comboText = self.qui.review_id_combo.currentText().split(":") return hglib.fromunicode(comboText[1]) def postReviewOpts(self, **opts): """Generate opts for reviewboard by form values""" opts['outgoingchanges'] = self.qui.outgoing_changes_check.isChecked() opts['branch'] = self.qui.branch_check.isChecked() opts['publish'] = self.qui.publish_immediately_check.isChecked() if self.qui.tab_widget.currentIndex() == 1: opts["existing"] = self.getReviewId() opts['update'] = self.qui.update_fields.isChecked() opts['summary'] = self.getSummary() else: opts['repoid'] = self.getRepoId() opts['summary'] = hglib.fromunicode(self.qui.summary_edit.currentText()) if (len(self.selectedRevs) > 1): #Set the parent to the revision below the last one on the list #so all checked revisions are included in the request opts['parent'] = str(self.selectedRevs[0] - 1) # Always use the upstream repo to determine the parent diff base # without the diff uploaded to review board dies opts['outgoing'] = True #Set the password just in case the user has opted to not save it opts['password'] = str(self.password) return opts def isValid(self): """Filled all required values?""" if not self.qui.repo_id_combo.currentText(): return False if self.qui.tab_widget.currentIndex() == 1: if not self.qui.review_id_combo.currentText(): return False if not self.allRevs: return False return True @pyqtSlot() def tabChanged(self): self.qui.post_review_button.setEnabled(self.isValid()) @pyqtSlot() def branchCheckToggle(self): if self.qui.branch_check.isChecked(): self.qui.outgoing_changes_check.setChecked(False) self.toggleOutgoingChangesets() @pyqtSlot() def outgoingChangesCheckToggle(self): if self.qui.outgoing_changes_check.isChecked(): self.qui.branch_check.setChecked(False) self.toggleOutgoingChangesets() def toggleOutgoingChangesets(self): branch = self.qui.branch_check.isChecked() outgoing = self.qui.outgoing_changes_check.isChecked() if branch or outgoing: self.initChangesets(self.allRevs, [self.selectedRevs.pop()]) self.qui.changesets_view.setEnabled(False) else: self.initChangesets(self.allRevs, self.allRevs) self.qui.changesets_view.setEnabled(True) def close(self): super(PostReviewDialog, self).close() def accept(self): if not self.isValid(): return if not self.password and not self.passwordPrompt(): return self.qui.progress_bar.show() self.qui.progress_label.setText("Posting Review...") self.qui.progress_label.show() def cmdargs(opts): args = [] for k, v in opts.iteritems(): if isinstance(v, bool): if v: args.append('--%s' % k.replace('_', '-')) else: for e in isinstance(v, basestring) and [v] or v: args += ['--%s' % k.replace('_', '-'), e] return args hglib.loadextension(self.ui, 'reviewboard') opts = self.postReviewOpts() revstr = str(self.selectedRevs.pop()) self.qui.post_review_button.setEnabled(False) self.qui.close_button.setEnabled(False) cmdline = map(hglib.tounicode, ['postreview'] + cmdargs(opts) + [revstr]) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) del self._cmdoutputs[:] sess.commandFinished.connect(self.onCompletion) sess.outputReceived.connect(self._captureOutput) @pyqtSlot() def onCompletion(self): self.qui.progress_bar.hide() self.qui.progress_label.hide() output = hglib.fromunicode(''.join(self._cmdoutputs), 'replace') saved = 'saved:' in output published = 'published:' in output if (saved or published): if saved: url = output.split('saved: ').pop().strip() msg = _('Review draft posted to %s\n') % url else: url = output.split('published: ').pop().strip() msg = _('Review published to %s\n') % url QDesktopServices.openUrl(QUrl(url)) qtlib.InfoMsgBox(_('Review Board'), _('Success'), msg, parent=self) else: error = output.split('abort: ').pop().strip() if error[:29] == "HTTP Error: basic auth failed": if self.passwordPrompt(): self.accept() else: self.qui.post_review_button.setEnabled(True) self.qui.close_button.setEnabled(True) return else: qtlib.ErrorMsgBox(_('Review Board'), _('Error'), error) self.writeSettings() super(PostReviewDialog, self).accept() @pyqtSlot(unicode, unicode) def _captureOutput(self, msg, label): if label != 'control': self._cmdoutputs.append(unicode(msg)) @pyqtSlot() def onSettingsButtonClicked(self): from tortoisehg.hgqt import settings if settings.SettingsDialog(parent=self, focus='reviewboard.server').exec_(): # not use repo.configChanged because it can clobber user input # accidentally. self.repo.invalidateui() # force reloading config immediately self.readSettings() tortoisehg-2.10/tortoisehg/hgqt/license.py0000664000076400007640000000442412100577421020024 0ustar stevesteve# license.py - license dialog for TortoiseHg # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuki KODAMA # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. """ TortoiseHg License dialog - PyQt4 version """ from PyQt4.QtCore import * from PyQt4.QtGui import * from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qtlib from tortoisehg.util import paths class LicenseDialog(QDialog): """Dialog for showing the TortoiseHg license""" def __init__(self, parent=None): super(LicenseDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('thg_logo')) self.setWindowTitle(_('License')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.resize(700, 400) self.lic_txt = QPlainTextEdit() self.lic_txt.setFont(QFont('Monospace')) self.lic_txt.setTextInteractionFlags( Qt.TextSelectableByKeyboard|Qt.TextSelectableByMouse) try: lic = open(paths.get_license_path(), 'rb').read() self.lic_txt.setPlainText(lic) except (IOError): pass self.hspacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.close_btn = QPushButton(_('&Close')) self.close_btn.clicked.connect(self.close) self.hbox = QHBoxLayout() self.hbox.addItem(self.hspacer) self.hbox.addWidget(self.close_btn) self.vbox = QVBoxLayout() self.vbox.setSpacing(6) self.vbox.addWidget(self.lic_txt) self.vbox.addLayout(self.hbox) self.setLayout(self.vbox) self._readsettings() self.setModal(True) def closeEvent(self, event): self._writesettings() super(LicenseDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('license/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('license/geom', self.saveGeometry()) def run(ui, *args, **opts): return LicenseDialog() tortoisehg-2.10/tortoisehg/hgqt/grep.py0000644000076400007640000007434612231647662017361 0ustar stevesteve# grep.py - Working copy and history search # # Copyright 2010 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import re from mercurial import ui, hg, error, commands, match, util, subrepo from tortoisehg.hgqt import htmlui, visdiff, qtlib, htmldelegate, thgrepo, cmdui, settings from tortoisehg.hgqt import filedialogs, fileview from tortoisehg.util import paths, hglib, thread2 from tortoisehg.hgqt.i18n import _ from PyQt4.QtCore import * from PyQt4.QtGui import * # This widget can be embedded in any application that would like to # provide search features class SearchWidget(QWidget, qtlib.TaskWidget): '''Working copy and repository search widget''' showMessage = pyqtSignal(QString) progress = pyqtSignal(QString, object, QString, QString, object) revisionSelected = pyqtSignal(int) def __init__(self, repoagent, upats, parent=None, **opts): QWidget.__init__(self, parent) self.thread = None mainvbox = QVBoxLayout() mainvbox.setSpacing(6) hbox = QHBoxLayout() hbox.setMargin(2) le = QLineEdit() if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### regular expression search pattern ###')) else: lbl = QLabel(_('Regexp:')) lbl.setBuddy(le) hbox.addWidget(lbl) chk = QCheckBox(_('Ignore case')) bt = QPushButton(_('Search')) bt.setDefault(True) f = bt.font() f.setWeight(QFont.Bold) bt.setFont(f) cbt = QPushButton(_('Stop')) cbt.setEnabled(False) cbt.clicked.connect(self.stopClicked) hbox.addWidget(le, 1) hbox.addWidget(chk) hbox.addWidget(bt) hbox.addWidget(cbt) incle = QLineEdit() excle = QLineEdit() working = QRadioButton(_('Working Copy')) revision = QRadioButton(_('Revision')) history = QRadioButton(_('All History')) singlematch = QCheckBox(_('Report only the first match per file')) follow = QCheckBox(_('Follow copies and renames')) recurse = QCheckBox(_('Recurse into subrepositories')) revle = QLineEdit() grid = QGridLayout() grid.addWidget(working, 0, 0) grid.addWidget(recurse, 0, 1) grid.addWidget(history, 1, 0) grid.addWidget(revision, 2, 0) grid.addWidget(revle, 2, 1) grid.addWidget(singlematch, 0, 3) grid.addWidget(follow, 0, 4) ilabel = QLabel(_('Includes:')) ilabel.setBuddy(incle) elabel = QLabel(_('Excludes:')) elabel.setBuddy(excle) ehelpstr = _('Comma separated list of exclusion file patterns. ' 'Exclusion patterns are applied after inclusion patterns.') ihelpstr = _('Comma separated list of inclusion file patterns. ' 'By default, the entire repository is searched.') if hasattr(incle, 'setPlaceholderText'): # Qt >= 4.7 incle.setPlaceholderText(u' '.join([u'###', ihelpstr, u'###'])) else: incle.setToolTip(ihelpstr) if hasattr(excle, 'setPlaceholderText'): # Qt >= 4.7 excle.setPlaceholderText(u' '.join([u'###', ehelpstr, u'###'])) else: excle.setToolTip(ehelpstr) grid.addWidget(ilabel, 1, 2) grid.addWidget(incle, 1, 3, 1, 2) grid.addWidget(elabel, 2, 2) grid.addWidget(excle, 2, 3, 1, 2) grid.setColumnStretch(3, 1) grid.setColumnStretch(1, 0) frame = QFrame() frame.setFrameStyle(QFrame.StyledPanel) def revisiontoggled(checked): revle.setEnabled(checked) if checked: revle.selectAll() QTimer.singleShot(0, lambda:revle.setFocus()) revision.toggled.connect(revisiontoggled) history.toggled.connect(singlematch.setDisabled) revle.setEnabled(False) revle.returnPressed.connect(self.runSearch) excle.returnPressed.connect(self.runSearch) incle.returnPressed.connect(self.runSearch) bt.clicked.connect(self.runSearch) def updateRecurse(checked): try: wctx = self.repo[None] if '.hgsubstate' in wctx: recurse.setEnabled(checked) else: recurse.setEnabled(False) recurse.setChecked(False) except Exception: recurse.setEnabled(False) recurse.setChecked(False) working.toggled.connect(updateRecurse) recurse.setChecked(True) working.setChecked(True) def updatefollow(): slowpath = bool(incle.text() or excle.text()) follow.setEnabled(history.isChecked() and not slowpath) if slowpath: follow.setChecked(False) history.toggled.connect(updatefollow) incle.textChanged.connect(updatefollow) excle.textChanged.connect(updatefollow) updatefollow() mainvbox.addLayout(hbox) frame.setLayout(grid) mainvbox.addWidget(frame) tv = MatchTree(repoagent, self) tv.revisionSelected.connect(self.revisionSelected) tv.setColumnHidden(COL_REVISION, True) tv.setColumnHidden(COL_USER, True) mainvbox.addWidget(tv) le.returnPressed.connect(self.runSearch) self._repoagent = repoagent repo = repoagent.rawRepo() self.tv, self.regexple, self.chk, self.recurse = tv, le, chk, recurse self.incle, self.excle, self.revle = incle, excle, revle self.wctxradio, self.ctxradio, self.aradio = working, revision, history self.singlematch, self.follow, self.eframe = singlematch, follow, frame self.searchbutton, self.cancelbutton = bt, cbt self.regexple.setFocus() if 'rev' in opts or 'all' in opts: self.setSearch(upats[0], **opts) elif len(upats) >= 1: le.setText(upats[0]) if len(upats) > 1: incle.setText(','.join(upats[1:])) chk.setChecked(opts.get('ignorecase', False)) repoid = str(repo[0]) s = QSettings() sh = list(s.value('grep/search-'+repoid).toStringList()) ph = list(s.value('grep/paths-'+repoid).toStringList()) self.pathshistory = [p for p in ph if p] self.searchhistory = [s for s in sh if s] self.setCompleters() if parent: self.closeonesc = False mainvbox.setContentsMargins(0, 0, 0, 0) self.setLayout(mainvbox) else: self.setWindowTitle(_('TortoiseHg Search')) self.resize(800, 550) self.closeonesc = True self.stbar = cmdui.ThgStatusBar() mainvbox.setContentsMargins(5, 5, 5, 5) outervbox = QVBoxLayout() outervbox.addLayout(mainvbox) outervbox.addWidget(self.stbar) outervbox.setContentsMargins(0, 0, 0, 0) self.setLayout(outervbox) self.showMessage.connect(self.stbar.showMessage) self.progress.connect(self.stbar.progress) @property def repo(self): return self._repoagent.rawRepo() def setCompleters(self): comp = QCompleter(self.searchhistory, self) QShortcut(QKeySequence('CTRL+D'), comp.popup(), self.onSearchCompleterDelete) self.regexple.setCompleter(comp) comp = QCompleter(self.pathshistory, self) QShortcut(QKeySequence('CTRL+D'), comp.popup(), self.onPathCompleterDelete) self.incle.setCompleter(comp) self.excle.setCompleter(comp) def onSearchCompleterDelete(self): 'CTRL+D pressed in search completer popup window' text = self.regexple.completer().currentCompletion() if text and text in self.searchhistory: self.searchhistory.remove(text) self.setCompleters() self.showMessage.emit(_('"%s" removed from search history') % text) def onPathCompleterDelete(self): 'CTRL+D pressed in path completer popup window' text = self.incle.completer().currentCompletion() if text and text in self.pathshistory: self.pathshistory.remove(text) self.setCompleters() self.showMessage.emit(_('"%s" removed from path history') % text) def addHistory(self, search, incpaths, excpaths): if search: usearch = hglib.tounicode(search) if usearch in self.searchhistory: self.searchhistory.remove(usearch) self.searchhistory = [usearch] + self.searchhistory[:9] for p in incpaths + excpaths: up = hglib.tounicode(p) if up in self.pathshistory: self.pathshistory.remove(up) self.pathshistory = [up] + self.pathshistory[:9] self.setCompleters() def setRevision(self, rev): 'Repowidget is forwarding a selected revision' if isinstance(rev, int): self.revle.setText(str(rev)) def setSearch(self, upattern, **opts): self.regexple.setText(upattern) if opts.get('all'): self.aradio.setChecked(True) elif opts.get('rev'): self.ctxradio.setChecked(True) self.revle.setText(opts['rev']) def stopClicked(self): if self.thread and self.thread.isRunning(): self.thread.cancel() self.thread.wait(2000) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: if self.thread and self.thread.isRunning(): self.stopClicked() elif self.closeonesc: self.close() else: return super(SearchWidget, self).keyPressEvent(event) def canExit(self): 'Repowidget is closing, can we quit?' if self.thread and self.thread.isRunning(): return False return True def saveSettings(self, s): repoid = str(self.repo[0]) s.setValue('grep/search-'+repoid, self.searchhistory) s.setValue('grep/paths-'+repoid, self.pathshistory) @pyqtSlot() def runSearch(self): """Run search for the current pattern in background thread""" if self.thread and self.thread.isRunning(): return model = self.tv.model() model.reset() pattern = hglib.fromunicode(self.regexple.text()) if not pattern: return try: icase = self.chk.isChecked() regexp = re.compile(pattern, icase and re.I or 0) except Exception, inst: msg = _('grep: invalid match pattern: %s\n') % \ hglib.tounicode(str(inst)) self.showMessage.emit(msg) return self.tv.setSortingEnabled(False) self.tv.pattern = pattern self.tv.icase = icase self.regexple.selectAll() inc = hglib.fromunicode(self.incle.text()) if inc: inc = inc.split(', ') exc = hglib.fromunicode(self.excle.text()) if exc: exc = exc.split(', ') rev = hglib.fromunicode(self.revle.text()).strip() self.addHistory(pattern, inc or [], exc or []) if self.wctxradio.isChecked(): self.tv.setColumnHidden(COL_REVISION, True) self.tv.setColumnHidden(COL_USER, True) ctx = self.repo[None] self.thread = CtxSearchThread(self.repo, regexp, ctx, inc, exc, self.singlematch.isChecked(), self.recurse.isChecked()) elif self.ctxradio.isChecked(): self.tv.setColumnHidden(COL_REVISION, True) self.tv.setColumnHidden(COL_USER, True) try: ctx = self.repo[rev or '.'] except error.RepoError, e: msg = _('grep: %s\n') % hglib.tounicode(str(e)) self.showMessage.emit(msg) return self.thread = CtxSearchThread(self.repo, regexp, ctx, inc, exc, self.singlematch.isChecked(), False) else: assert self.aradio.isChecked() self.tv.setColumnHidden(COL_REVISION, False) self.tv.setColumnHidden(COL_USER, False) self.thread = HistorySearchThread(self.repo, pattern, icase, inc, exc, follow=self.follow.isChecked()) self.showMessage.emit('') self.regexple.setEnabled(False) self.searchbutton.setEnabled(False) self.cancelbutton.setEnabled(True) self.thread.finished.connect(self.searchfinished) self.thread.showMessage.connect(self.showMessage) self.thread.progress.connect(self.progress) self.thread.matchedRow.connect( lambda wrapper: model.appendRow(*wrapper.data)) self.thread.start() def reload(self): # TODO pass def searchfinished(self): self.cancelbutton.setEnabled(False) self.searchbutton.setEnabled(True) self.regexple.setEnabled(True) self.regexple.setFocus() count = self.tv.model().rowCount(None) if count: for col in xrange(COL_TEXT): self.tv.resizeColumnToContents(col) self.tv.setSortingEnabled(True) if self.thread.completed == False: # do not overwrite error message on failure pass elif count: self.showMessage.emit(_('%d matches found') % count) else: self.showMessage.emit(_('No matches found')) class DataWrapper(object): def __init__(self, data): self.data = data class HistorySearchThread(QThread): '''Background thread for searching repository history''' matchedRow = pyqtSignal(DataWrapper) showMessage = pyqtSignal(unicode) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repo, pattern, icase, inc, exc, follow): super(HistorySearchThread, self).__init__() self.repo = hg.repository(repo.ui, repo.root) self.pattern = pattern self.icase = icase self.inc = inc self.exc = exc self.follow = follow self.completed = False def cancel(self): if self.isRunning() and hasattr(self, 'thread_id'): try: thread2._async_raise(self.thread_id, KeyboardInterrupt) except ValueError: pass def run(self): haskbf = settings.hasExtension('kbfiles') haslf = settings.hasExtension('largefiles') self.thread_id = int(QThread.currentThreadId()) def emitrow(row): w = DataWrapper(row) self.matchedRow.emit(w) def emitprog(topic, pos, item, unit, total): self.progress.emit(topic, pos, item, unit, total) class incrui(ui.ui): fullmsg = '' def write(self, msg, *args, **opts): self.fullmsg += msg if self.fullmsg.count('\0') >= 6: try: fname, line, rev, addremove, user, text, tail = \ self.fullmsg.split('\0', 6) if haslf and thgrepo.isLfStandin(fname): raise ValueError if (haslf or haskbf) and thgrepo.isBfStandin(fname): raise ValueError text = hglib.tounicode(text) text = Qt.escape(text) text = '%s %s' % (addremove, text) fname = hglib.tounicode(fname) user = hglib.tounicode(user) row = [fname, int(rev), int(line), user, text] emitrow(row) except ValueError: pass self.fullmsg = tail def progress(topic, pos, item='', unit='', total=None): emitprog(topic, pos, item, unit, total) cwd = os.getcwd() os.chdir(self.repo.root) self.progress.emit(*cmdui.startProgress(_('Searching'), _('history'))) try: # hg grep [-i] -afn regexp opts = {'all':True, 'user':True, 'follow':self.follow, 'rev':[], 'line_number':True, 'print0':True, 'ignore_case':self.icase, 'include':self.inc, 'exclude':self.exc} u = incrui() commands.grep(u, self.repo, self.pattern, **opts) except Exception, e: self.showMessage.emit(str(e)) except KeyboardInterrupt: self.showMessage.emit(_('Interrupted')) self.progress.emit(*cmdui.stopProgress(_('Searching'))) os.chdir(cwd) self.completed = True class CtxSearchThread(QThread): '''Background thread for searching a changectx''' matchedRow = pyqtSignal(object) showMessage = pyqtSignal(unicode) progress = pyqtSignal(QString, object, QString, QString, object) def __init__(self, repo, regexp, ctx, inc, exc, once, recurse): super(CtxSearchThread, self).__init__() self.repo = hg.repository(repo.ui, repo.root) self.regexp = regexp self.ctx = ctx self.inc = inc self.exc = exc self.once = once self.recurse = recurse self.canceled = False self.completed = False def cancel(self): self.canceled = True def run(self): def badfn(f, msg): e = hglib.tounicode("%s: %s" % (matchfn.rel(f), msg)) self.showMessage.emit(e) self.hu = htmlui.htmlui() try: # generate match function relative to repo root matchfn = match.match(self.repo.root, '', [], self.inc, self.exc) matchfn.bad = badfn self.searchRepo(self.ctx, '', matchfn) self.completed = True except Exception, e: self.showMessage.emit(hglib.tounicode(str(e))) def searchRepo(self, ctx, prefix, matchfn): topic = _('Searching') unit = _('files') total = len(ctx.manifest()) count = 0 haskbf = settings.hasExtension('kbfiles') haslf = settings.hasExtension('largefiles') for wfile in ctx: # walk manifest if self.canceled: break if haslf and thgrepo.isLfStandin(wfile): continue if (haslf or haskbf) and thgrepo.isBfStandin(wfile): continue self.progress.emit(topic, count, wfile, unit, total) count += 1 if not matchfn(wfile): continue try: data = ctx[wfile].data() # load file data except EnvironmentError: self.showMessage.emit(_('Skipping %s, unable to read') % hglib.tounicode(wfile)) continue if util.binary(data): continue for i, line in enumerate(data.splitlines()): pos = 0 for m in self.regexp.finditer(line): # perform regexp self.hu.write(line[pos:m.start()], label='ui.status') self.hu.write(line[m.start():m.end()], label='grep.match') pos = m.end() if pos: self.hu.write(line[pos:], label='ui.status') path = os.path.join(prefix, wfile) row = [hglib.tounicode(path), i + 1, ctx.rev(), None, hglib.tounicode(self.hu.getdata()[0])] w = DataWrapper(row) self.matchedRow.emit(w) if self.once: break self.progress.emit(topic, None, '', '', None) if ctx.rev() is None and self.recurse: for s in ctx.substate: if not matchfn(s): continue sub = ctx.sub(s) if isinstance(sub, subrepo.hgsubrepo): newprefix = os.path.join(prefix, s) self.searchRepo(sub._repo[None], newprefix, lambda x: True) COL_PATH = 0 COL_LINE = 1 COL_REVISION = 2 # Hidden if ctx COL_USER = 3 # Hidden if ctx COL_TEXT = 4 class MatchTree(QTableView): revisionSelected = pyqtSignal(int) contextmenu = None def __init__(self, repoagent, parent): QTableView.__init__(self, parent) self._repoagent = repoagent self.pattern = None self.icase = False self.embedded = parent.parent() is not None self.selectedRows = () self.delegate = htmldelegate.HTMLDelegate(self) self.setItemDelegateForColumn(COL_TEXT, self.delegate) self.setSelectionMode(QTableView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setContextMenuPolicy(Qt.CustomContextMenu) self.setShowGrid(False) vh = self.verticalHeader() vh.hide() vh.setDefaultSectionSize(20) self.horizontalHeader().setStretchLastSection(True) self._filedialogs = qtlib.DialogKeeper(MatchTree._createFileDialog, MatchTree._genFileDialogKey, self) self.actions = {} self.contextmenu = QMenu(self) for key, name, func, shortcut in ( ('edit', _('Vi&ew File'), self.onViewFile, 'CTRL+E'), ('ctx', _('&View Changeset'), self.onViewChangeset, 'CTRL+V'), ('vdiff', _('&Diff to Parent'), self.onVisualDiff, 'CTRL+D'), ('ann', _('Annotate &File'), self.onAnnotateFile, 'CTRL+F')): action = QAction(name, self) action.triggered.connect(func) action.setShortcut(QKeySequence(shortcut)) self.actions[key] = action self.addAction(action) self.contextmenu.addAction(action) self.activated.connect(self.onRowActivated) self.customContextMenuRequested.connect(self.menuRequest) self.setModel(MatchModel(self)) self.selectionModel().selectionChanged.connect(self.onSelectionChanged) @property def repo(self): return self._repoagent.rawRepo() def dragObject(self): snapshots = {} for index in self.selectionModel().selectedRows(): path, line, rev, user, text = self.model().getRow(index) if rev not in snapshots: snapshots[rev] = [path] else: snapshots[rev].append(path) urls = [] for rev, paths in snapshots.iteritems(): if rev is not None: base, _ = visdiff.snapshot(self.repo, paths, self.repo[rev]) else: base = self.repo.root for p in paths: urls.append(QUrl.fromLocalFile(os.path.join(base, path))) if urls: d = QDrag(self) m = QMimeData() m.setUrls(urls) d.setMimeData(m) d.start(Qt.CopyAction) def mousePressEvent(self, event): self.pressPos = event.pos() self.pressTime = QTime.currentTime() return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): d = event.pos() - self.pressPos if d.manhattanLength() < QApplication.startDragDistance(): return QTableView.mouseMoveEvent(self, event) elapsed = self.pressTime.msecsTo(QTime.currentTime()) if elapsed < QApplication.startDragTime(): return QTableView.mouseMoveEvent(self, event) self.dragObject() return QTableView.mouseMoveEvent(self, event) def menuRequest(self, point): if not self.selectionModel().selectedRows(): return point = self.viewport().mapToGlobal(point) self.contextmenu.exec_(point) def onSelectionChanged(self, selected, deselected): selrows = [] wctxonly = True allhistory = False for index in self.selectionModel().selectedRows(): path, line, rev, user, text = self.model().getRow(index) if rev is not None: wctxonly = False if user is not None: allhistory = True selrows.append((rev, path, line)) self.selectedRows = selrows self.actions['ctx'].setEnabled(not wctxonly and self.embedded) self.actions['vdiff'].setEnabled(allhistory) def onRowActivated(self, index): saved = self.selectedRows path, line, rev, user, text = self.model().getRow(index) self.selectedRows = [(rev, path, line)] self.onAnnotateFile() self.selectedRows = saved def onAnnotateFile(self): repo = self.repo seen = set() for rev, upath, line in self.selectedRows: path = hglib.fromunicode(upath) # Only open one annotate instance per file if path in seen: continue else: seen.add(path) if rev is None and path not in repo[None]: abs = repo.wjoin(path) root = paths.find_root(abs) if root and abs.startswith(root): path = abs[len(root)+1:] # TODO: do not instantiate repo here srepo = thgrepo.repository(None, root) srepoagent = srepo._pyqtobj self._openAnnotateDialog(srepoagent, rev, path, line) else: continue else: self._openAnnotateDialog(self._repoagent, rev, path, line) def _openAnnotateDialog(self, repoagent, rev, path, line): if rev is None: repo = repoagent.rawRepo() rev = repo['.'].rev() dlg = self._filedialogs.open(repoagent, path) dlg.setFileViewMode(fileview.AnnMode) dlg.goto(rev) dlg.showLine(line) dlg.setSearchPattern(hglib.tounicode(self.pattern)) dlg.setSearchCaseInsensitive(self.icase) def _createFileDialog(self, repoagent, path): return filedialogs.FileLogDialog(repoagent, path) def _genFileDialogKey(self, repoagent, path): repo = repoagent.rawRepo() return repo.wjoin(path) def onViewChangeset(self): for rev, path, line in self.selectedRows: self.revisionSelected.emit(int(rev)) return def onViewFile(self): repo, ui, pattern = self.repo, self.repo.ui, self.pattern seen = set() for rev, upath, line in self.selectedRows: path = hglib.fromunicode(upath) # Only open one editor instance per file if path in seen: continue else: seen.add(path) if rev is None: qtlib.editfiles(repo, [path], line, pattern, self) else: base, _ = visdiff.snapshot(repo, [path], repo[rev]) files = [os.path.join(base, path)] qtlib.editfiles(repo, files, line, pattern, self) def onVisualDiff(self): rows = self.selectedRows[:] repo, ui = self.repo, self.repo.ui while rows: defer = [] crev = rows[0][0] files = set([rows[0][1]]) for rev, path, line in rows[1:]: if rev == crev: files.add(path) else: defer.append([rev, path, line]) if crev is not None: dlg = visdiff.visualdiff(ui, repo, map(hglib.fromunicode, files), {'change':crev}) if dlg: dlg.exec_() rows = defer class MatchModel(QAbstractTableModel): def __init__(self, parent): QAbstractTableModel.__init__(self, parent) self.rows = [] self.headers = (_('File'), _('Line'), _('Rev'), _('User'), _('Match Text')) def rowCount(self, parent): return len(self.rows) def columnCount(self, parent): return len(self.headers) def data(self, index, role): if not index.isValid(): return QVariant() if role == Qt.DisplayRole: return QVariant(self.rows[index.row()][index.column()]) return QVariant() def headerData(self, col, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return QVariant() else: return QVariant(self.headers[col]) def flags(self, index): flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled return flags def sort(self, col, order): self.layoutAboutToBeChanged.emit() self.rows.sort(key=lambda x: x[col], reverse=(order == Qt.DescendingOrder)) self.layoutChanged.emit() ## Custom methods def appendRow(self, *args): l = len(self.rows) self.beginInsertRows(QModelIndex(), l, l) self.rows.append(args) self.endInsertRows() self.layoutChanged.emit() def reset(self): self.beginRemoveRows(QModelIndex(), 0, len(self.rows)-1) self.rows = [] self.endRemoveRows() self.layoutChanged.emit() def getRow(self, index): assert index.isValid() return self.rows[index.row()] class SearchDialog(QDialog): def __init__(self, repoagent, upats, parent=None, **opts): super(SearchDialog, self).__init__(parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon('view-filter')) self.setLayout(QVBoxLayout(self)) self._searchwidget = SearchWidget(repoagent, upats, parent=self, **opts) self.layout().addWidget(self._searchwidget) def closeEvent(self, event): if not self._searchwidget.canExit(): self._searchwidget.stopClicked() event.ignore() return self._searchwidget.saveSettings(QSettings()) super(SearchDialog, self).closeEvent(event) def setSearch(self, upattern, **opts): self._searchwidget.setSearch(upattern, **opts) @pyqtSlot() def runSearch(self): self._searchwidget.runSearch() tortoisehg-2.10/tortoisehg/hgqt/qfold.py0000644000076400007640000001157712231647662017526 0ustar stevesteve# qfold.py - QFold dialog for TortoiseHg # # Copyright 2010 Steve Borho # Copyright 2010 Johan Samyn # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from hgext import mq from tortoisehg.util import hglib from tortoisehg.hgqt.i18n import _ from tortoisehg.hgqt import qscilib, qtlib, messageentry class QFoldDialog(QDialog): def __init__(self, repoagent, patches, parent): super(QFoldDialog, self).__init__(parent) self._repoagent = repoagent repo = repoagent.rawRepo() self.setWindowTitle(_('Patch fold - %s') % repo.displayname) self.setWindowIcon(qtlib.geticon('hg-qfold')) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self.setLayout(QVBoxLayout()) mlbl = QLabel(_('New patch message:')) self.layout().addWidget(mlbl) self.msgte = messageentry.MessageEntry(self) self.msgte.installEventFilter(qscilib.KeyPressInterceptor(self)) self.layout().addWidget(self.msgte) self.keepchk = QCheckBox(_('Keep patch files')) self.keepchk.setChecked(True) self.layout().addWidget(self.keepchk) q = self.repo.mq q.parseseries() patches = [p for p in q.series if p in patches] class PatchListWidget(QListWidget): def __init__(self, parent): QListWidget.__init__(self, parent) self.setCurrentRow(0) def focusInEvent(self, event): i = self.item(self.currentRow()) if i: self.parent().parent().showSummary(i) QListWidget.focusInEvent(self, event) def dropEvent(self, event): QListWidget.dropEvent(self, event) spp = self.parent().parent() spp.msgte.setText(spp.composeMsg(self.getPatchList())) def getPatchList(self): return [hglib.fromunicode(self.item(i).text()) \ for i in xrange(0, self.count())] ugb = QGroupBox(_('Patches to fold')) ugb.setLayout(QVBoxLayout()) ugb.layout().setContentsMargins(*(0,)*4) self.ulw = PatchListWidget(self) self.ulw.setDragDropMode(QListView.InternalMove) ugb.layout().addWidget(self.ulw) self.ulw.currentItemChanged.connect(lambda: self.showSummary(self.ulw.item(self.ulw.currentRow()))) self.layout().addWidget(ugb) for p in patches: item = QListWidgetItem(hglib.tounicode(p)) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) self.ulw.addItem(item) slbl = QLabel(_('Summary:')) self.layout().addWidget(slbl) self.summ = QTextEdit() self.summ.setFont(qtlib.getfont('fontcomment').font()) self.summ.setMaximumHeight(80) self.summ.setReadOnly(True) self.summ.setFocusPolicy(Qt.NoFocus) self.layout().addWidget(self.summ) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Ok|BB.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) self.bbox = bbox self._repoagent.configChanged.connect(self.configChanged) self._readsettings() self.msgte.setText(self.composeMsg(patches)) self.msgte.refresh(self.repo) @property def repo(self): return self._repoagent.rawRepo() def showSummary(self, item): patchname = hglib.fromunicode(item.text()) txt = '\n'.join(mq.patchheader(self.repo.mq.join(patchname)).message) self.summ.setText(hglib.tounicode(txt)) def composeMsg(self, patches): return u'\n* * *\n'.join( [hglib.tounicode(self.repo.changectx(p).description()) for p in ['qtip'] + patches]) @pyqtSlot() def configChanged(self): '''Repository is reporting its config files have changed''' self.msgte.refresh(self.repo) def options(self): return {'keep': self.keepchk.isChecked(), 'message': unicode(self.msgte.text())} def patches(self): return map(hglib.tounicode, self.ulw.getPatchList()) def accept(self): self._writesettings() QDialog.accept(self) def closeEvent(self, event): self._writesettings() super(QFoldDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(s.value('qfold/geom').toByteArray()) def _writesettings(self): s = QSettings() s.setValue('qfold/geom', self.saveGeometry()) tortoisehg-2.10/tortoisehg/util/0000755000076400007640000000000012235634575016051 5ustar stevestevetortoisehg-2.10/tortoisehg/util/version.py0000644000076400007640000000422312110205646020072 0ustar stevesteve# version.py - TortoiseHg version # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import ui, hg, commands, error from tortoisehg.util.i18n import _ def liveversion(): 'Attempt to read the version from the live repository' utilpath = os.path.dirname(os.path.realpath(__file__)) thgpath = os.path.dirname(os.path.dirname(utilpath)) if not os.path.isdir(os.path.join(thgpath, '.hg')): raise error.RepoError(_('repository %s not found') % thgpath) u = ui.ui() repo = hg.repository(u, path=thgpath) u.pushbuffer() commands.identify(u, repo, id=True, tags=True, rev='.') l = u.popbuffer().split() while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags l.pop() if len(l) > 1: # tag found version = l[-1] if l[0].endswith('+'): # propagate the dirty status to the tag version += '+' elif len(l) == 1: # no tag found u.pushbuffer() commands.parents(u, repo, template='{latesttag}+{latesttagdistance}-') version = u.popbuffer() + l[0] return repo[None].branch(), version def version(): try: branch, version = liveversion() return version except: pass try: import __version__ return __version__.version except ImportError: return _('unknown') def package_version(): try: branch, version = liveversion() extra = None if '+' in version: version, extra = version.split('+', 1) v = [int(x) for x in version.split('.')] while len(v) < 3: v.append(0) major, minor, periodic = v if extra != None: tagdistance = int(extra.split('-', 1)[0]) periodic *= 10000 if branch == 'default': periodic += tagdistance + 5000 else: periodic += tagdistance + 1000 return '.'.join([str(x) for x in (major, minor, periodic)]) except: pass return _('unknown') tortoisehg-2.10/tortoisehg/util/thread2.py0000664000076400007640000000350412100577421017743 0ustar stevesteve# Interuptible threads # # http://sebulba.wikispaces.com/recipe+thread2 # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import threading import inspect import ctypes def _async_raise(tid, exctype): """raises the exception, performs cleanup if needed""" if not inspect.isclass(exctype): raise TypeError("Only types can be raised (not instances)") res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype)) if res == 0: raise ValueError("invalid thread id") elif res != 1: # """if it returns a number greater than one, you're in trouble, # and you should call it again with exc=NULL to revert the effect""" ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0) raise SystemError("PyThreadState_SetAsyncExc failed") class Thread(threading.Thread): def _get_my_tid(self): """determines this (self's) thread id""" if not self.isAlive(): raise threading.ThreadError("the thread is not active") # do we have it cached? if hasattr(self, "_thread_id"): return self._thread_id # no, look for it in the _active dict for tid, tobj in threading._active.items(): if tobj is self: self._thread_id = tid return tid raise AssertionError("could not determine the thread's id") def raise_exc(self, exctype): """raises the given exception type in the context of this thread""" _async_raise(self._get_my_tid(), exctype) def terminate(self): """raises SystemExit in the context of the given thread, which should cause the thread to exit silently (unless caught)""" self.raise_exc(SystemExit) tortoisehg-2.10/tortoisehg/util/editor.py0000644000076400007640000000752012135406415017702 0ustar stevesteveimport os, sys from mercurial import util, match def _getplatformexecutablekey(): if sys.platform == 'darwin': key = 'executable-osx' elif os.name == 'nt': key = 'executable-win' else: key = 'executable-unix' return key _platformexecutablekey = _getplatformexecutablekey() def _toolstr(ui, tool, part, default=""): return ui.config("editor-tools", tool + "." + part, default) toolcache = {} def _findtool(ui, tool): global toolcache if tool in toolcache: return toolcache[tool] for kn in ("regkey", "regkeyalt"): k = _toolstr(ui, tool, kn) if not k: continue p = util.lookupreg(k, _toolstr(ui, tool, "regname")) if p: p = util.findexe(p + _toolstr(ui, tool, "regappend")) if p: toolcache[tool] = p return p global _platformexecutablekey exe = _toolstr(ui, tool, _platformexecutablekey) if not exe: exe = _toolstr(ui, tool, 'executable', tool) path = util.findexe(util.expandpath(exe)) if path: toolcache[tool] = path return path elif tool != exe: path = util.findexe(tool) toolcache[tool] = path return path toolcache[tool] = None return None def _findeditor(repo, files): '''returns tuple of editor name and editor path. tools matched by pattern are returned as (name, toolpath) tools detected by search are returned as (name, toolpath) tortoisehg.editor is returned as (None, tortoisehg.editor) HGEDITOR or ui.editor are returned as (None, ui.editor) So first return value is an [editor-tool] name or None and second return value is a toolpath or user configured command line ''' ui = repo.ui # first check for tool specified by file patterns. The first file pattern # which matches one of the files being edited selects the editor for pat, tool in ui.configitems("editor-patterns"): mf = match.match(repo.root, '', [pat]) toolpath = _findtool(ui, tool) if mf(files[0]) and toolpath: return (tool, util.shellquote(toolpath)) # then editor-tools tools = {} for k, v in ui.configitems("editor-tools"): t = k.split('.')[0] if t not in tools: try: priority = int(_toolstr(ui, t, "priority", "0")) except ValueError, e: priority = -100 tools[t] = priority names = tools.keys() tools = sorted([(-p, t) for t, p in tools.items()]) editor = ui.config('tortoisehg', 'editor') if editor: if editor not in names: # if tortoisehg.editor does not match an editor-tools entry, take # the value directly return (None, editor) # else select this editor as highest priority (may still use another if # it is not found on this machine) tools.insert(0, (None, editor)) for p, t in tools: toolpath = _findtool(ui, t) if toolpath: return (t, util.shellquote(toolpath)) # fallback to potential CLI editor editor = os.environ.get('HGEDITOR') or repo.ui.config('ui', 'editor') \ or os.environ.get('EDITOR', 'vi') return (None, editor) def detecteditor(repo, files): 'returns tuple of editor tool path and arguments' name, pathorconfig = _findeditor(repo, files) if name is None: return (pathorconfig, None, None, None) else: args = _toolstr(repo.ui, name, "args") argsln = _toolstr(repo.ui, name, "argsln") argssearch = _toolstr(repo.ui, name, "argssearch") return (pathorconfig, args, argsln, argssearch) def findeditors(ui): seen = set() for key, value in ui.configitems('editor-tools'): t = key.split('.')[0] seen.add(t) return [t for t in seen if _findtool(ui, t)] tortoisehg-2.10/tortoisehg/util/cachethg.py0000644000076400007640000001711612110205646020160 0ustar stevesteve# cachethg.py - overlay/status cache # # Copyright 2008 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import sys from mercurial import hg, util, ui, node, merge, error, scmutil from tortoisehg.util import paths, debugthg, hglib debugging = False enabled = True localonly = False includepaths = [] excludepaths = [] try: from _winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx from win32api import GetTickCount CACHE_TIMEOUT = 5000 try: hkey = OpenKey(HKEY_CURRENT_USER, r"Software\TortoiseHg") enabled = QueryValueEx(hkey, 'EnableOverlays')[0] in ('1', 'True') localonly = QueryValueEx(hkey, 'LocalDisksOnly')[0] in ('1', 'True') incs = QueryValueEx(hkey, 'IncludePath')[0] excs = QueryValueEx(hkey, 'ExcludePath')[0] debugging = QueryValueEx(hkey, 'OverlayDebug')[0] in ('1', 'True') for p in incs.split(';'): path = p.strip() if path: includepaths.append(path) for p in excs.split(';'): path = p.strip() if path: excludepaths.append(path) except EnvironmentError: pass except ImportError: from time import time as GetTickCount CACHE_TIMEOUT = 5.0 debugging = debugthg.debug('O') if debugging: debugf = debugthg.debugf debugf('Enabled %s', enabled) debugf('LocalDisksOnly %s', localonly) debugf('IncludePaths %s', includepaths) debugf('ExcludePaths %s', excludepaths) else: debugf = debugthg.debugf_No STATUS_STATES = 'MAR!?IC' MODIFIED, ADDED, REMOVED, DELETED, UNKNOWN, IGNORED, UNCHANGED = STATUS_STATES NOT_IN_REPO = ' ' ROOT = "r" UNRESOLVED = 'U' # file status cache overlay_cache = {} cache_tick_count = 0 cache_root = None cache_pdir = None def add_dirs(list): dirs = set() if list: dirs.add('') for f in list: pdir = os.path.dirname(f) if pdir in dirs: continue while pdir: dirs.add(pdir) pdir = os.path.dirname(pdir) list.extend(dirs) def get_state(upath, repo=None): """ Get the state of a given path in source control. """ states = get_states(upath, repo) return states and states[0] or NOT_IN_REPO def get_states(upath, repo=None): """ Get the states of a given path in source control. """ global overlay_cache, cache_tick_count global cache_root, cache_pdir global enabled, localonly global includepaths, excludepaths #debugf("called: _get_state(%s)", path) tc = GetTickCount() try: # handle some Asian charsets path = upath.encode('mbcs') except: path = upath # check if path is cached pdir = os.path.dirname(path) status = overlay_cache.get(path, '') if overlay_cache and (cache_pdir == pdir or cache_pdir and status not in ' r' and path.startswith(cache_pdir)): #use cached data when pdir has not changed or when the cached state is a repo state if tc - cache_tick_count < CACHE_TIMEOUT: if not status: if os.path.isdir(os.path.join(path, '.hg')): add(path, ROOT) status = ROOT else: status = overlay_cache.get(pdir + '*', NOT_IN_REPO) add(path, status) debugf("%s: %s (cached~)", (path, status)) else: debugf("%s: %s (cached)", (path, status)) return status else: debugf("Timed out!!") overlay_cache.clear() cache_tick_count = GetTickCount() # path is a drive if path.endswith(":\\"): add(path, NOT_IN_REPO) return NOT_IN_REPO # open repo if cache_pdir == pdir: root = cache_root else: debugf("find new root") root = paths.find_root(path) if root == path: if not overlay_cache: cache_root = pdir add(path, ROOT) debugf("%s: r", path) return ROOT cache_root = root cache_pdir = pdir if root is None: debugf("_get_state: not in repo") overlay_cache = {None: None} cache_tick_count = GetTickCount() return NOT_IN_REPO debugf("_get_state: root = " + root) hgdir = os.path.join(root, '.hg', '') if pdir == hgdir[:-1] or pdir.startswith(hgdir): add(pdir, NOT_IN_REPO) return NOT_IN_REPO try: if not enabled: overlay_cache = {None: None} cache_tick_count = GetTickCount() debugf("overlayicons disabled") return NOT_IN_REPO if localonly and paths.netdrive_status(path): debugf("%s: is a network drive", path) overlay_cache = {None: None} cache_tick_count = GetTickCount() return NOT_IN_REPO if includepaths: for p in includepaths: if path.startswith(p): break else: debugf("%s: is not in an include path", path) overlay_cache = {None: None} cache_tick_count = GetTickCount() return NOT_IN_REPO for p in excludepaths: if path.startswith(p): debugf("%s: is in an exclude path", path) overlay_cache = {None: None} cache_tick_count = GetTickCount() return NOT_IN_REPO tc1 = GetTickCount() real = os.path.realpath #only test if necessary (symlink in path) if not repo or (repo.root != root and repo.root != real(root)): repo = hg.repository(ui.ui(), path=root) debugf("hg.repository() took %g ticks", (GetTickCount() - tc1)) except error.RepoError: # We aren't in a working tree debugf("%s: not in repo", pdir) add(pdir + '*', IGNORED) return IGNORED except Exception, e: debugf("error while handling %s:", pdir) debugf(e) add(pdir + '*', UNKNOWN) return UNKNOWN # get file status tc1 = GetTickCount() try: matcher = scmutil.match(repo[None], [pdir]) repostate = repo.status(match=matcher, ignored=True, clean=True, unknown=True) except util.Abort, inst: debugf("abort: %s", inst) debugf("treat as unknown : %s", path) return UNKNOWN debugf("status() took %g ticks", (GetTickCount() - tc1)) mergestate = repo.dirstate.parents()[1] != node.nullid and \ hasattr(merge, 'mergestate') # cached file info tc = GetTickCount() overlay_cache = {} add(root, ROOT) add(os.path.join(root, '.hg'), NOT_IN_REPO) states = STATUS_STATES if mergestate: mstate = merge.mergestate(repo) unresolved = [f for f in mstate if mstate[f] == 'u'] if unresolved: modified = repostate[0] modified[:] = set(modified) - set(unresolved) repostate.insert(0, unresolved) states = [UNRESOLVED] + states states = zip(repostate, states) states[-1], states[-2] = states[-2], states[-1] #clean before ignored for grp, st in states: add_dirs(grp) for f in grp: fpath = os.path.join(root, os.path.normpath(f)) add(fpath, st) status = overlay_cache.get(path, UNKNOWN) debugf("%s: %s", (path, status)) cache_tick_count = GetTickCount() return status def add(path, state): overlay_cache[path] = overlay_cache.get(path, '') + state tortoisehg-2.10/tortoisehg/util/__version__.py0000644000076400007640000000007212235634575020703 0ustar stevesteve# this file is autogenerated by setup.py version = "2.10" tortoisehg-2.10/tortoisehg/util/menuthg.py0000644000076400007640000002642112110205646020060 0ustar stevesteve# menuthg.py - TortoiseHg shell extension menu # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os from mercurial import hg, ui, node, error from tortoisehg.util.i18n import _ as gettext from tortoisehg.util import cachethg, paths, hglib def _(msgid): return {'id': msgid, 'str': gettext(msgid)} thgcmenu = { 'commit': { 'label': _('Commit...'), 'help': _('Commit changes in repository'), 'icon': 'menucommit.ico'}, 'init': { 'label': _('Create Repository Here'), 'help': _('Create a new repository'), 'icon': 'menucreaterepos.ico'}, 'clone': { 'label': _('Clone...'), 'help': _('Create clone here from source'), 'icon': 'menuclone.ico'}, 'status': { 'label': _('File Status'), 'help': _('Repository status & changes'), 'icon': 'menushowchanged.ico'}, 'add': { 'label': _('Add Files...'), 'help': _('Add files to version control'), 'icon': 'menuadd.ico'}, 'revert': { 'label': _('Revert Files...'), 'help': _('Revert file changes'), 'icon': 'menurevert.ico'}, 'forget': { 'label': _('Forget Files...'), 'help': _('Remove files from version control'), 'icon': 'menurevert.ico'}, 'remove': { 'label': _('Remove Files...'), 'help': _('Remove files from version control'), 'icon': 'menudelete.ico'}, 'rename': { 'label': _('Rename File'), 'help': _('Rename file or directory'), 'icon': 'general.ico'}, 'workbench': { 'label': _('Workbench'), 'help': _('View change history in repository'), 'icon': 'menulog.ico'}, 'log': { 'label': _('File History'), 'help': _('View change history of selected files'), 'icon': 'menulog.ico'}, 'shelve': { 'label': _('Shelve Changes'), 'help': _('Move changes between working dir and patch'), 'icon': 'menucommit.ico'}, 'synch': { 'label': _('Synchronize'), 'help': _('Synchronize with remote repository'), 'icon': 'menusynch.ico'}, 'serve': { 'label': _('Web Server'), 'help': _('Start web server for this repository'), 'icon': 'proxy.ico'}, 'update': { 'label': _('Update...'), 'help': _('Update working directory'), 'icon': 'menucheckout.ico'}, 'thgstatus': { 'label': _('Update Icons'), 'help': _('Update icons for this repository'), 'icon': 'refresh_overlays.ico'}, 'userconf': { 'label': _('Global Settings'), 'help': _('Configure user wide settings'), 'icon': 'settings_user.ico'}, 'repoconf': { 'label': _('Repository Settings'), 'help': _('Configure repository settings'), 'icon': 'settings_repo.ico'}, 'shellconf': { 'label': _('Explorer Extension Settings'), 'help': _('Configure Explorer extension'), 'icon': 'settings_user.ico'}, 'about': { 'label': _('About TortoiseHg'), 'help': _('Show About Dialog'), 'icon': 'menuabout.ico'}, 'vdiff': { 'label': _('Diff to parent'), 'help': _('View changes using GUI diff tool'), 'icon': 'TortoiseMerge.ico'}, 'hgignore': { 'label': _('Edit Ignore Filter'), 'help': _('Edit repository ignore filter'), 'icon': 'ignore.ico'}, 'guess': { 'label': _('Guess Renames'), 'help': _('Detect renames and copies'), 'icon': 'detect_rename.ico'}, 'grep': { 'label': _('Search History'), 'help': _('Search file revisions for patterns'), 'icon': 'menurepobrowse.ico'}, 'dndsynch': { 'label': _('DnD Synchronize'), 'help': _('Synchronize with dragged repository'), 'icon': 'menusynch.ico'}} _ALWAYS_DEMOTE_ = ('about', 'userconf', 'repoconf') class TortoiseMenu(object): def __init__(self, menutext, helptext, hgcmd, icon=None, state=True): self.menutext = menutext self.helptext = helptext self.hgcmd = hgcmd self.icon = icon self.state = state def isSubmenu(self): return False def isSep(self): return False class TortoiseSubmenu(TortoiseMenu): def __init__(self, menutext, helptext, menus=[], icon=None): TortoiseMenu.__init__(self, menutext, helptext, None, icon) self.menus = menus[:] def add_menu(self, menutext, helptext, hgcmd, icon=None, state=True): self.menus.append(TortoiseMenu(menutext, helptext, hgcmd, icon, state)) def add_sep(self): self.menus.append(TortoiseMenuSep()) def get_menus(self): return self.menus def append(self, entry): self.menus.append(entry) def isSubmenu(self): return True class TortoiseMenuSep(object): hgcmd = '----' def isSubmenu(self): return False def isSep(self): return True class thg_menu(object): def __init__(self, ui, promoted, name = "TortoiseHg"): self.menus = [[]] self.ui = ui self.name = name self.sep = [False] self.promoted = promoted def add_menu(self, hgcmd, icon=None, state=True): if hgcmd in self.promoted: pos = 0 else: pos = 1 while len(self.menus) <= pos: #add Submenu self.menus.append([]) self.sep.append(False) if self.sep[pos]: self.sep[pos] = False self.menus[pos].append(TortoiseMenuSep()) self.menus[pos].append(TortoiseMenu( thgcmenu[hgcmd]['label']['str'], thgcmenu[hgcmd]['help']['str'], hgcmd, thgcmenu[hgcmd]['icon'], state)) def add_sep(self): self.sep = [True for _s in self.sep] def get(self): menu = self.menus[0][:] for submenu in self.menus[1:]: menu.append(TortoiseSubmenu(self.name, 'Mercurial', submenu, "hg.ico")) menu.append(TortoiseMenuSep()) return menu def __iter__(self): return iter(self.get()) def open_repo(path): root = paths.find_root(path) if root: try: repo = hg.repository(ui.ui(), path=root) return repo except error.RepoError: pass except StandardError, e: print "error while opening repo %s:" % path print e return None class menuThg: """shell extension that adds context menu items""" def __init__(self, internal=False): self.name = "TortoiseHg" promoted = [] pl = ui.ui().config('tortoisehg', 'promoteditems', 'commit,log') for item in pl.split(','): item = item.strip() if item: promoted.append(item) if internal: for item in thgcmenu.keys(): promoted.append(item) for item in _ALWAYS_DEMOTE_: if item in promoted: promoted.remove(item) self.promoted = promoted def get_commands_dragdrop(self, srcfiles, destfolder): """ Get a list of commands valid for the current selection. Commands are instances of TortoiseMenu, TortoiseMenuSep or TortoiseMenu """ # we can only accept dropping one item if len(srcfiles) > 1: return [] # open repo drag_repo = None drop_repo = None drag_path = srcfiles[0] drag_repo = open_repo(drag_path) if not drag_repo: return [] if drag_repo and drag_repo.root != drag_path: return [] # dragged item must be a hg repo root directory drop_repo = open_repo(destfolder) menu = thg_menu(drag_repo.ui, self.promoted, self.name) menu.add_menu('clone') if drop_repo: menu.add_menu('dndsynch') return menu def get_norepo_commands(self, cwd, files): menu = thg_menu(ui.ui(), self.promoted, self.name) menu.add_menu('clone') menu.add_menu('init') menu.add_menu('userconf') menu.add_sep() menu.add_menu('about') menu.add_sep() return menu def get_commands(self, repo, cwd, files): """ Get a list of commands valid for the current selection. Commands are instances of TortoiseMenu, TortoiseMenuSep or TortoiseMenu """ states = set() onlyfiles = len(files) > 0 hashgignore = False for f in files: if not os.path.isfile(f): onlyfiles = False if f.endswith('.hgignore'): hashgignore = True states.update(cachethg.get_states(f, repo)) if not files: states.update(cachethg.get_states(cwd, repo)) if cachethg.ROOT in states and len(states) == 1: states.add(cachethg.MODIFIED) changed = bool(states & set([cachethg.ADDED, cachethg.MODIFIED])) modified = cachethg.MODIFIED in states clean = cachethg.UNCHANGED in states tracked = changed or modified or clean new = bool(states & set([cachethg.UNKNOWN, cachethg.IGNORED])) menu = thg_menu(repo.ui, self.promoted, self.name) if changed or cachethg.UNKNOWN in states or 'qtip' in repo['.'].tags(): menu.add_menu('commit') if hashgignore or new and len(states) == 1: menu.add_menu('hgignore') if changed or cachethg.UNKNOWN in states: menu.add_menu('status') # Visual Diff (any extdiff command) has_vdiff = repo.ui.config('tortoisehg', 'vdiff', 'vdiff') != '' if has_vdiff and modified: menu.add_menu('vdiff') if len(files) == 0 and cachethg.UNKNOWN in states: menu.add_menu('guess') elif len(files) == 1 and tracked: # needs ico menu.add_menu('rename') if files and new: menu.add_menu('add') if files and tracked: menu.add_menu('remove') if files and changed: menu.add_menu('revert') menu.add_sep() if tracked: menu.add_menu(files and 'log' or 'workbench') if len(files) == 0: menu.add_sep() menu.add_menu('grep') menu.add_sep() menu.add_menu('synch') menu.add_menu('serve') menu.add_sep() menu.add_menu('clone') if repo.root != cwd: menu.add_menu('init') # add common menu items menu.add_sep() menu.add_menu('userconf') if tracked: menu.add_menu('repoconf') menu.add_menu('about') menu.add_sep() return menu tortoisehg-2.10/tortoisehg/util/i18n.py0000644000076400007640000000553512110205646017173 0ustar stevesteve# i18n.py - TortoiseHg internationalization code # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import gettext, os, locale from mercurial import util from tortoisehg.util import paths _localeenvs = ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG') def _defaultlanguage(): if os.name != 'nt' or util.any(e in os.environ for e in _localeenvs): return # honor posix-style env var # On Windows, UI language can be determined by GetUserDefaultUILanguage(), # but gettext doesn't take it into account. # Note that locale.getdefaultlocale() uses GetLocaleInfo(), which may be # different from UI language. # # For details, please read "User Interface Language Management": # http://msdn.microsoft.com/en-us/library/dd374098(v=VS.85).aspx try: from ctypes import windll # requires Python>=2.5 langid = windll.kernel32.GetUserDefaultUILanguage() return locale.windows_locale[langid] except (ImportError, AttributeError, KeyError): pass def setlanguage(lang=None): """Change translation catalog to the specified language""" global t, language if not lang: lang = _defaultlanguage() opts = {} if lang: opts['languages'] = (lang,) t = gettext.translation('tortoisehg', paths.get_locale_path(), fallback=True, **opts) language = lang or locale.getdefaultlocale(_localeenvs)[0] setlanguage() def availablelanguages(): """List up language code of which message catalog is available""" basedir = paths.get_locale_path() def mopath(lang): return os.path.join(basedir, lang, 'LC_MESSAGES', 'tortoisehg.mo') if os.path.exists(basedir): # locale/ is an install option langs = [e for e in os.listdir(basedir) if os.path.exists(mopath(e))] else: langs = [] langs.append('en') # means null translation return sorted(langs) def _(message, context=''): if context: sep = '\004' tmsg = t.gettext(context + sep + message) if sep not in tmsg: return tmsg return t.gettext(message) def ngettext(singular, plural, n): return t.ngettext(singular, plural, n) def agettext(message, context=''): """Translate message and convert to local encoding such as 'ascii' before being returned. Only use this if you need to output translated messages to command-line interface (ie: Windows Command Prompt). """ try: from tortoisehg.util import hglib u = _(message, context) return hglib.fromutf(u) except (LookupError, UnicodeEncodeError): return message class keepgettext(object): def _(self, message, context=''): return {'id': message, 'str': _(message, context)} tortoisehg-2.10/tortoisehg/util/paths.py0000644000076400007640000001315712231647662017546 0ustar stevesteve# paths.py - TortoiseHg path utilities # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. try: from config import icon_path, bin_path, license_path, locale_path except ImportError: icon_path, bin_path, license_path, locale_path = None, None, None, None import os, sys import mercurial _hg_command = None def find_root(path=None): p = path or os.getcwd() while not os.path.isdir(os.path.join(p, ".hg")): oldp = p p = os.path.dirname(p) if p == oldp: return None if not os.access(p, os.R_OK): return None return p def get_tortoise_icon(icon): "Find a tortoisehg icon" icopath = os.path.join(get_icon_path(), icon) if os.path.isfile(icopath): return icopath else: print 'icon not found', icon return None def get_icon_path(): global icon_path return icon_path or os.path.join(get_prog_root(), 'icons') def get_license_path(): global license_path return license_path or os.path.join(get_prog_root(), 'COPYING.txt') def get_locale_path(): global locale_path return locale_path or os.path.join(get_prog_root(), 'locale') def _get_hg_path(): return os.path.abspath(os.path.join(mercurial.__file__, '..', '..')) def get_hg_command(): """List of command to execute hg (equivalent to mercurial.util.hgcmd)""" global _hg_command if _hg_command is None: _hg_command = _find_hg_command() return _hg_command if os.name == 'nt': import _winreg import win32net import win32file def find_in_path(pgmname): "return first executable found in search path" global bin_path ospath = os.environ['PATH'].split(os.pathsep) ospath.insert(0, bin_path or get_prog_root()) pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD') pathext = pathext.lower().split(os.pathsep) for path in ospath: ppath = os.path.join(path, pgmname) for ext in pathext: if os.path.exists(ppath + ext): return ppath + ext return None def _find_hg_command(): if hasattr(sys, 'frozen'): progdir = get_prog_root() exe = os.path.join(progdir, 'hg.exe') if os.path.exists(exe): return [exe] # look for in-place build, i.e. "make local" exe = os.path.join(_get_hg_path(), 'hg.exe') if os.path.exists(exe): return [exe] exe = find_in_path('hg') if not exe: return ['hg.exe'] if exe.endswith('.bat'): # assumes Python script exists in the same directory. .bat file # has problems like "Terminate Batch job?" prompt on Ctrl-C. if hasattr(sys, 'frozen'): python = find_in_path('python') or 'python' else: python = sys.executable return [python, exe[:-4]] return [exe] def get_prog_root(): if getattr(sys, 'frozen', False): try: return _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, r"Software\TortoiseHg") except: pass return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) def is_unc_path(path): unc, rest = os.path.splitunc(path) return bool(unc) def is_on_fixed_drive(path): if is_unc_path(path): # All UNC paths (\\host\mount) are considered not-fixed return False drive, remain = os.path.splitdrive(path) if drive: return win32file.GetDriveType(drive) == win32file.DRIVE_FIXED else: return False USE_OK = 0 # network drive status def netdrive_status(drive): """ return True if a network drive is accessible (connected, ...), or False if is not a network drive """ if hasattr(os.path, 'splitunc'): unc, rest = os.path.splitunc(drive) if unc: # All UNC paths (\\host\mount) are considered nonlocal return True letter = os.path.splitdrive(drive)[0].upper() _drives, total, _ = win32net.NetUseEnum(None, 1, 0) for drv in _drives: if drv['local'] == letter: info = win32net.NetUseGetInfo(None, letter, 1) return info['status'] == USE_OK return False else: # Not Windows def find_in_path(pgmname): """ return first executable found in search path """ global bin_path ospath = os.environ['PATH'].split(os.pathsep) ospath.insert(0, bin_path or get_prog_root()) for path in ospath: ppath = os.path.join(path, pgmname) if os.access(ppath, os.X_OK): return ppath return None def _find_hg_command(): # look for in-place build, i.e. "make local" exe = os.path.join(_get_hg_path(), 'hg') if os.path.exists(exe): return [exe] exe = find_in_path('hg') if not exe: return ['hg'] return [exe] def get_prog_root(): path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return path def netdrive_status(drive): """ return True if a network drive is accessible (connected, ...), or False if is not a network drive """ return False def is_unc_path(path): return False def is_on_fixed_drive(path): return True tortoisehg-2.10/tortoisehg/util/shlib.py0000644000076400007640000001554112110205646017513 0ustar stevesteve# shlib.py - TortoiseHg shell utilities # # Copyright 2007 TK Soh # Copyright 2008 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import sys import time import threading from mercurial.i18n import _ from mercurial import hg def get_system_times(): t = os.times() if t[4] == 0.0: # Windows leaves this as zero, so use time.clock() t = (t[0], t[1], t[2], t[3], time.clock()) return t if os.name == 'nt': def browse_url(url): try: import win32api except ImportError: return def start_browser(): try: win32api.ShellExecute(0, 'open', url, None, None, 0) except Exception: pass threading.Thread(target=start_browser).start() def shell_notify(paths, noassoc=False): try: from win32com.shell import shell, shellcon import pywintypes except ImportError: return dirs = set() for path in paths: if path is None: continue abspath = os.path.abspath(path) if not os.path.isdir(abspath): abspath = os.path.dirname(abspath) dirs.add(abspath) # send notifications to deepest directories first for dir in sorted(dirs, key=len, reverse=True): try: pidl, ignore = shell.SHILCreateFromPath(dir, 0) except pywintypes.com_error: return if pidl is None: continue shell.SHChangeNotify(shellcon.SHCNE_UPDATEITEM, shellcon.SHCNF_IDLIST | shellcon.SHCNF_FLUSH, pidl, None) if not noassoc: shell.SHChangeNotify(shellcon.SHCNE_ASSOCCHANGED, shellcon.SHCNF_FLUSH, None, None) def update_thgstatus(ui, root, wait=False): '''Rewrite the file .hg/thgstatus Caches the information provided by repo.status() in the file .hg/thgstatus, which can then be read by the overlay shell extension to display overlay icons for directories. The file .hg/thgstatus contains one line for each directory that has removed, modified or added files (in that order of preference). Each line consists of one char for the status of the directory (r, m or a), followed by the relative path of the directory in the repo. If the file .hg/thgstatus is empty, then the repo's working directory is clean. Specify wait=True to wait until the system clock ticks to the next second before accessing Mercurial's dirstate. This is useful when Mercurial's .hg/dirstate contains unset entries (in output of "hg debugstate"). unset entries happen if .hg/dirstate was updated within the same second as Mercurial updated the respective file in the working tree. This happens with a high probability for example when cloning a repo. The overlay shell extension will display unset dirstate entries as (potentially false) modified. Specifying wait=True ensures that there are no unset entries left in .hg/dirstate when this function exits. ''' if wait: tref = time.time() tdelta = float(int(tref)) + 1.0 - tref if (tdelta > 0.0): time.sleep(tdelta) repo = hg.repository(ui, root) # a fresh repo object is needed repo.bfstatus = True repo.lfstatus = True repostate = repo.status() # will update .hg/dirstate as a side effect repo.bfstatus = False repo.lfstatus = False modified, added, removed, deleted = repostate[:4] dirstatus = {} def dirname(f): return '/'.join(f.split('/')[:-1]) for fn in added: dirstatus[dirname(fn)] = 'a' for fn in modified: dirstatus[dirname(fn)] = 'm' for fn in removed + deleted: dirstatus[dirname(fn)] = 'r' update = False f = None try: try: f = repo.opener('thgstatus', 'rb') for dn in sorted(dirstatus): s = dirstatus[dn] e = f.readline() if e.startswith('@@noicons'): break if e == '' or e[0] != s or e[1:-1] != dn: update = True break if f.readline() != '': # extra line in f, needs update update = True except IOError: update = True finally: if f != None: f.close() if update: f = repo.opener('thgstatus', 'wb', atomictemp=True) for dn in sorted(dirstatus): s = dirstatus[dn] f.write(s + dn + '\n') ui.note("%s %s\n" % (s, dn)) if hasattr(f, 'rename'): # On Mercurial 1.9 and earlier, there was a rename() function # that served the purpose now served by close(), while close() # served the purpose now served by discard(). f.rename() else: f.close() return update else: def shell_notify(paths, noassoc=False): if not paths: return notify = os.environ.get('THG_NOTIFY', '.tortoisehg/notify') if not os.path.isabs(notify): notify = os.path.join(os.path.expanduser('~'), notify) os.environ['THG_NOTIFY'] = notify if not os.path.isfile(notify): return try: f_notify = open(notify, 'w') except IOError: return try: abspaths = [os.path.abspath(path) for path in paths if path] f_notify.write('\n'.join(abspaths)) finally: f_notify.close() def update_thgstatus(*args, **kws): pass def browse_url(url): def start_browser(): if sys.platform == 'darwin': # use Mac OS X internet config module (removed in Python 3.0) import ic ic.launchurl(url) else: try: import gconf client = gconf.client_get_default() browser = client.get_string( '/desktop/gnome/url-handlers/http/command') + '&' os.system(browser % url) except ImportError: # If gconf is not found, fall back to old standard os.system('firefox ' + url) threading.Thread(target=start_browser).start() tortoisehg-2.10/tortoisehg/util/wconfig.py0000644000076400007640000001775612215326102020055 0ustar stevesteve# wconfig.py - Writable config object wrapper # # Copyright 2010 Yuya Nishihara # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import cStringIO import ConfigParser from mercurial import error, util, config as config_mod try: from iniparse import INIConfig _hasiniparse = True except ImportError: _hasiniparse = False if _hasiniparse: try: from iniparse import change_comment_syntax # iniparse>=0.3.2 change_comment_syntax(allow_rem=False) except (ImportError, TypeError): # TODO: yet need to care about iniparse<0.3.2 ?? import re from iniparse.ini import CommentLine # Monkypatch this regex to prevent iniparse from considering # 'rem' as a comment CommentLine.regex = re.compile(r'^(?P[%;#])(?P.*)$') class _wsortdict(object): """Wrapper for config.sortdict to record set/del operations""" def __init__(self, dict): self._dict = dict self._log = [] # log of set/del operations # no need to wrap copy() since we don't keep trac of it. def __contains__(self, key): return key in self._dict def __getitem__(self, key): return self._dict[key] def __setitem__(self, key, val): self._setdict(key, val) self._logset(key, val) def _logset(self, key, val): """Record set operation to log; called also by _wconfig""" def op(target): target[key] = val self._log.append(op) def _setdict(self, key, val): if key not in self._dict: self._dict[key] = val # append return # preserve current order def get(k): if k == key: return val else: return self._dict[k] for k in list(self._dict): self._dict[k] = get(k) def __iter__(self): return iter(self._dict) def __len__(self): return len(self._dict) def update(self, src): self._dict.update(src) self._logupdate(src) def _logupdate(self, src): """Record update operation to log; called also by _wconfig""" for k in src: self._logset(k, src[k]) def __delitem__(self, key): del self._dict[key] self._logdel(key) def _logdel(self, key): """Record del operation to log""" def op(target): try: del target[key] except KeyError: # in case somebody else deleted it pass self._log.append(op) def __getattr__(self, name): return getattr(self._dict, name) def _replaylog(self, target): """Replay operations against the given target; called by _wconfig""" for op in self._log: op(target) class _wconfig(object): """Wrapper for config.config to replay changes to iniparse on write This records set/del operations and replays them on write(). Source file is reloaded before replaying changes, so that it doesn't override changes for another part of file made by somebody else: - A "set foo = bar", B "set baz = bax" => "foo = bar, baz = bax" - A "set foo = bar", B "set foo = baz" => "foo = baz" (last one wins) - A "del foo", B "set foo = baz" => "foo = baz" (last one wins) - A "set foo = bar", B "del foo" => "" (last one wins) """ def __init__(self, data=None): self._config = config_mod.config(data) self._readfiles = [] # list of read (path, fp, sections, remap) self._sections = {} if isinstance(data, self.__class__): # keep log self._readfiles.extend(data._readfiles) self._sections.update(data._sections) elif data: # record as changes self._logupdates(data) def copy(self): return self.__class__(self) def __contains__(self, section): return section in self._config def __getitem__(self, section): try: return self._sections[section] except KeyError: if self._config[section]: self._sections[section] = _wsortdict(self._config[section]) return self._sections[section] else: return {} def __iter__(self): return iter(self._config) def update(self, src): self._config.update(src) self._logupdates(src) def _logupdates(self, src): for s in src: self[s]._logupdate(src[s]) def set(self, section, item, value, source=''): self._setconfig(section, item, value, source) self[section]._logset(item, value) def _setconfig(self, section, item, value, source): if item not in self._config[section]: # need to handle 'source' self._config.set(section, item, value, source) else: self[section][item] = value def remove(self, section, item): del self[section][item] self[section]._logdel(item) def read(self, path, fp=None, sections=None, remap=None): self._config.read(path, fp, sections, remap) self._readfiles.append((path, fp, sections, remap)) def write(self, dest): ini = self._readini() self._replaylogs(ini) dest.write(str(ini)) def _readini(self): """Create iniparse object by reading every file""" if len(self._readfiles) > 1: raise NotImplementedError("wconfig does not support read() more " "than once") def newini(fp=None): try: # TODO: optionxformvalue isn't used by INIConfig ? return INIConfig(fp=fp, optionxformvalue=None) except ConfigParser.MissingSectionHeaderError, err: raise error.ParseError(err.message.splitlines()[0], '%s:%d' % (err.filename, err.lineno)) except ConfigParser.ParsingError, err: if err.errors: loc = '%s:%d' % (err.filename, err.errors[0][0]) else: loc = err.filename raise error.ParseError(err.message.splitlines()[0], loc) if not self._readfiles: return newini() path, fp, sections, remap = self._readfiles[0] if sections: raise NotImplementedError("wconfig does not support 'sections'") if remap: raise NotImplementedError("wconfig does not support 'remap'") if fp: fp.seek(0) return newini(fp) else: fp = util.posixfile(path, 'rb') try: return newini(fp) finally: fp.close() def _replaylogs(self, ini): def getsection(ini, section): if section in ini: return ini[section] else: newns = getattr(ini, '_new_namespace', getattr(ini, 'new_namespace')) return newns(section) for k, v in self._sections.iteritems(): v._replaylog(getsection(ini, k)) def __getattr__(self, name): return getattr(self._config, name) def config(data=None): """Create writable config if iniparse available; otherwise readonly obj You can test whether the returned obj is writable or not by `hasattr(obj, 'write')`. """ if _hasiniparse: return _wconfig(data) else: return config_mod.config(data) def readfile(path): """Read the given file to return config object""" c = config() c.read(path) return c def writefile(config, path): """Write the given config obj to the specified file""" f = util.atomictempfile(os.path.realpath(path), 'w') try: buf = cStringIO.StringIO() config.write(buf) # normalize line endings for line in buf.getvalue().splitlines(): f.write(line + '\n') f.close() finally: del f # unlink temp file tortoisehg-2.10/tortoisehg/util/__init__.py0000664000076400007640000000001512100577421020143 0ustar stevesteve#placeholder tortoisehg-2.10/tortoisehg/util/thgstatus.py0000664000076400007640000000315712100577421020444 0ustar stevesteve# thgstatus.py - update TortoiseHg status cache # # Copyright 2009 Adrian Buehlmann # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. '''update TortoiseHg status cache''' from mercurial import hg from tortoisehg.util import paths, shlib import os def cachefilepath(repo): return repo.join("thgstatus") def run(_ui, *pats, **opts): if opts.get('all'): roots = [] base = os.getcwd() for f in os.listdir(base): r = paths.find_root(os.path.join(base, f)) if r is not None: roots.append(r) for r in roots: _ui.note("%s\n" % r) shlib.update_thgstatus(_ui, r, wait=False) shlib.shell_notify([r]) return root = paths.find_root() if opts.get('repository'): root = opts.get('repository') if root is None: _ui.status("no repository\n") return repo = hg.repository(_ui, root) if opts.get('remove'): try: os.remove(cachefilepath(repo)) except OSError: pass return if opts.get('show'): try: f = open(cachefilepath(repo), 'rb') for e in f: _ui.status("%s %s\n" % (e[0], e[1:-1])) f.close() except IOError: _ui.status("*no status*\n") return wait = opts.get('delay') is not None shlib.update_thgstatus(_ui, root, wait=wait) if opts.get('notify'): shlib.shell_notify(opts.get('notify')) _ui.note("thgstatus updated\n") tortoisehg-2.10/tortoisehg/util/hglib.py0000644000076400007640000006015612231647662017515 0ustar stevesteve# hglib.py - Mercurial API wrappers for TortoiseHg # # Copyright 2007 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import os import shlex import time from mercurial import ui, util, extensions, match from mercurial import encoding, templatefilters, filemerge, error, scmutil from mercurial import dispatch as hgdispatch _encoding = encoding.encoding _encodingmode = encoding.encodingmode _fallbackencoding = encoding.fallbackencoding # extensions which can cause problem with TortoiseHg _extensions_blacklist = ('color', 'pager', 'progress') from tortoisehg.util import paths from tortoisehg.util.hgversion import hgversion from tortoisehg.util.i18n import _, ngettext def tounicode(s): """ Convert the encoding of string from MBCS to Unicode. Based on mercurial.util.tolocal(). Return 'unicode' type string. """ if s is None: return None if isinstance(s, unicode): return s if isinstance(s, encoding.localstr): return s._utf8.decode('utf-8') for e in ('utf-8', _encoding): try: return s.decode(e, 'strict') except UnicodeDecodeError: pass return s.decode(_fallbackencoding, 'replace') def fromunicode(s, errors='strict'): """ Convert the encoding of string from Unicode to MBCS. Return 'str' type string. If you don't want an exception for conversion failure, specify errors='replace'. """ if s is None: return None s = unicode(s) # s can be QtCore.QString for enc in (_encoding, _fallbackencoding): try: l = s.encode(enc) if s == l.decode(enc): return l # non-lossy encoding return encoding.localstr(s.encode('utf-8'), l) except UnicodeEncodeError: pass l = s.encode(_encoding, errors) # last ditch return encoding.localstr(s.encode('utf-8'), l) def toutf(s): """ Convert the encoding of string from MBCS to UTF-8. Return 'str' type string. """ if s is None: return None if isinstance(s, encoding.localstr): return s._utf8 return tounicode(s).encode('utf-8').replace('\0','') def fromutf(s): """ Convert the encoding of string from UTF-8 to MBCS Return 'str' type string. """ if s is None: return None try: return fromunicode(s.decode('utf-8'), 'replace') except UnicodeDecodeError: # can't round-trip return str(fromunicode(s.decode('utf-8', 'replace'), 'replace')) def _getfirstrevisionlabel(repo, ctx): # see context.changectx for look-up order of labels bookmarks = ctx.bookmarks() if ctx in repo.parents(): # keep bookmark unchanged when updating to current rev if repo._bookmarkcurrent in bookmarks: return repo._bookmarkcurrent else: # more common switching bookmark, rather than deselecting it if bookmarks: return bookmarks[0] tags = ctx.tags() if tags: return tags[0] branch = ctx.branch() if repo.branchtip(branch) == ctx.node(): return branch def getrevisionlabel(repo, rev): """Return symbolic name for the specified revision or stringfy it""" if rev is None: return None # no symbol for working revision ctx = repo[rev] label = _getfirstrevisionlabel(repo, ctx) if label and ctx == repo[label]: return label return str(rev) _hidetags = None def gethidetags(ui): global _hidetags if _hidetags is None: tags = toutf(ui.config('tortoisehg', 'hidetags', '')) taglist = [t.strip() for t in tags.split()] _hidetags = taglist return _hidetags def getrawctxtags(changectx): '''Returns the tags for changectx, converted to UTF-8 but unfiltered for hidden tags''' value = [toutf(tag) for tag in changectx.tags()] if len(value) == 0: return None return value def getctxtags(changectx): '''Returns all unhidden tags for changectx, converted to UTF-8''' value = getrawctxtags(changectx) if value: htlist = gethidetags(changectx._repo.ui) tags = [tag for tag in value if tag not in htlist] if len(tags) == 0: return None return tags return None def getmqpatchtags(repo): '''Returns all tag names used by MQ patches, or []''' if hasattr(repo, 'mq'): repo.mq.parseseries() return repo.mq.series[:] else: return [] def getcurrentqqueue(repo): """Return the name of the current patch queue.""" if not hasattr(repo, 'mq'): return None cur = os.path.basename(repo.mq.path) if cur.startswith('patches-'): cur = cur[8:] return cur def _applymovemqpatches(q, after, patches): fullindexes = dict((q.guard_re.split(rpn, 1)[0], i) for i, rpn in enumerate(q.fullseries)) fullmap = {} # patch: line in series file for i, n in sorted([(fullindexes[n], n) for n in patches], reverse=True): fullmap[n] = q.fullseries.pop(i) del fullindexes # invalid if after is None: fullat = 0 else: for i, rpn in enumerate(q.fullseries): if q.guard_re.split(rpn, 1)[0] == after: fullat = i + 1 break else: fullat = len(q.fullseries) # last ditch (should not happen) q.fullseries[fullat:fullat] = (fullmap[n] for n in patches) q.parseseries() q.seriesdirty = True # maybe this can be implemented as hg extension def movemqpatches(repo, after, patches): """Move the given patches after the specified patch, or to the beginning of the series if after is None""" q = repo.mq if util.any(n not in q.series for n in patches): raise ValueError('unknown patch to move specified') if after in patches: raise ValueError('invalid patch position specified') if util.any(q.isapplied(n) for n in patches): raise ValueError('cannot move applied patches') if after is None: at = 0 else: at = q.series.index(after) + 1 if at < q.seriesend(True): raise ValueError('cannot move into applied patches') try: wlock = repo.wlock(False) # no wait to avoid blocking GUI thread except error.LockHeld: return False try: _applymovemqpatches(q, after, patches) try: q.savedirty() return True except EnvironmentError: return False finally: wlock.release() def enabledextensions(): """Return the {name: shortdesc} dict of enabled extensions shortdesc is in local encoding. """ return extensions.enabled() def disabledextensions(): return extensions.disabled() def allextensions(): """Return the {name: shortdesc} dict of known extensions shortdesc is in local encoding. """ enabledexts = enabledextensions() disabledexts = disabledextensions() exts = (disabledexts or {}).copy() exts.update(enabledexts) return exts def validateextensions(enabledexts): """Report extensions which should be disabled Returns the dict {name: message} of extensions expected to be disabled. message is 'utf-8'-encoded string. """ exts = {} if os.name != 'posix': exts['inotify'] = _('inotify is not supported on this platform') if 'win32text' in enabledexts: exts['eol'] = _('eol is incompatible with win32text') if 'eol' in enabledexts: exts['win32text'] = _('win32text is incompatible with eol') if 'perfarce' in enabledexts: exts['hgsubversion'] = _('hgsubversion is incompatible with perfarce') if 'hgsubversion' in enabledexts: exts['perfarce'] = _('perfarce is incompatible with hgsubversion') return exts def loadextension(ui, name): # Between Mercurial revisions 1.2 and 1.3, extensions.load() stopped # calling uisetup() after loading an extension. This could do # unexpected things if you use an hg version < 1.3 extensions.load(ui, name, None) mod = extensions.find(name) uisetup = getattr(mod, 'uisetup', None) if uisetup: uisetup(ui) def _loadextensionwithblacklist(orig, ui, name, path): if name.startswith('hgext.') or name.startswith('hgext/'): shortname = name[6:] else: shortname = name if shortname in _extensions_blacklist and not path: # only bundled ext return return orig(ui, name, path) def wrapextensionsloader(): """Wrap extensions.load(ui, name) for blacklist to take effect""" extensions.wrapfunction(extensions, 'load', _loadextensionwithblacklist) def canonpaths(list): 'Get canonical paths (relative to root) for list of files' # This is a horrible hack. Please remove this when HG acquires a # decent case-folding solution. canonpats = [] cwd = os.getcwd() root = paths.find_root(cwd) for f in list: try: canonpats.append(scmutil.canonpath(root, cwd, f)) except util.Abort: # Attempt to resolve case folding conflicts. fu = f.upper() cwdu = cwd.upper() if fu.startswith(cwdu): canonpats.append(scmutil.canonpath(root, cwd, f[len(cwd+os.sep):])) else: # May already be canonical canonpats.append(f) return canonpats def escapepath(path): 'Before passing a file path to hg API, it may need escaping' p = path if '[' in p or '{' in p or '*' in p or '?' in p: return 'path:' + p else: return p def normpats(pats): 'Normalize file patterns' normpats = [] for pat in pats: kind, p = match._patsplit(pat, None) if kind: normpats.append(pat) else: if '[' in p or '{' in p or '*' in p or '?' in p: normpats.append('glob:' + p) else: normpats.append('path:' + p) return normpats def mergetools(ui, values=None): 'returns the configured merge tools and the internal ones' if values == None: values = [] seen = values[:] for key, value in ui.configitems('merge-tools'): t = key.split('.')[0] if t not in seen: seen.append(t) # Ensure the tool is installed if filemerge._findtool(ui, t): values.append(t) values.append('internal:merge') values.append('internal:prompt') values.append('internal:dump') values.append('internal:local') values.append('internal:other') values.append('internal:fail') return values _difftools = None def difftools(ui): global _difftools if _difftools: return _difftools def fixup_extdiff(diffopts): if '$child' not in diffopts: diffopts.append('$parent1') diffopts.append('$child') if '$parent2' in diffopts: mergeopts = diffopts[:] diffopts.remove('$parent2') else: mergeopts = [] return diffopts, mergeopts tools = {} for cmd, path in ui.configitems('extdiff'): if cmd.startswith('cmd.'): cmd = cmd[4:] if not path: path = cmd diffopts = ui.config('extdiff', 'opts.' + cmd, '') diffopts = shlex.split(diffopts) diffopts, mergeopts = fixup_extdiff(diffopts) tools[cmd] = [path, diffopts, mergeopts] elif cmd.startswith('opts.'): continue else: # command = path opts if path: diffopts = shlex.split(path) path = diffopts.pop(0) else: path, diffopts = cmd, [] diffopts, mergeopts = fixup_extdiff(diffopts) tools[cmd] = [path, diffopts, mergeopts] mt = [] mergetools(ui, mt) for t in mt: if t.startswith('internal:'): continue dopts = ui.config('merge-tools', t + '.diffargs', '') mopts = ui.config('merge-tools', t + '.diff3args', '') dopts, mopts = shlex.split(dopts), shlex.split(mopts) tools[t] = [filemerge._findtool(ui, t), dopts, mopts] _difftools = tools return tools tortoisehgtoollocations = ( ('workbench.custom-toolbar', _('Workbench custom toolbar')), ('workbench.revdetails.custom-menu', _('Revision details context menu')), ('workbench.commit.custom-menu', _('Commit context menu')), ('workbench.filelist.custom-menu', _('File context menu (on manifest ' 'and revision details)')), ) def tortoisehgtools(uiorconfig, selectedlocation=None): """Parse 'tortoisehg-tools' section of ini file. >>> from pprint import pprint >>> from mercurial import config >>> class memui(ui.ui): ... def readconfig(self, filename, root=None, trust=False, ... sections=None, remap=None): ... pass # avoid reading settings from file-system Changes: >>> hgrctext = ''' ... [tortoisehg-tools] ... update_to_tip.icon = hg-update ... update_to_tip.command = hg update tip ... update_to_tip.tooltip = Update to tip ... ''' >>> uiobj = memui() >>> uiobj._tcfg.parse('', hgrctext) into the following dictionary >>> tools, toollist = tortoisehgtools(uiobj) >>> pprint(tools) #doctest: +NORMALIZE_WHITESPACE {'update_to_tip': {'command': 'hg update tip', 'icon': 'hg-update', 'tooltip': 'Update to tip'}} >>> toollist ['update_to_tip'] If selectedlocation is set, only return those tools that have been configured to be shown at the given "location". Tools are added to "locations" by adding them to one of the "extension lists", which are lists of tool names, which follow the same format as the workbench.task-toolbar setting, i.e. a list of tool names, separated by spaces or "|" to indicate separators. >>> hgrctext_full = hgrctext + ''' ... update_to_null.icon = hg-update ... update_to_null.command = hg update null ... update_to_null.tooltip = Update to null ... explore_wd.command = explorer.exe /e,{ROOT} ... explore_wd.enable = iswd ... explore_wd.label = Open in explorer ... explore_wd.showoutput = True ... ... [tortoisehg] ... workbench.custom-toolbar = update_to_tip | explore_wd ... workbench.revdetails.custom-menu = update_to_tip update_to_null ... ''' >>> uiobj = memui() >>> uiobj._tcfg.parse('', hgrctext_full) >>> tools, toollist = tortoisehgtools( ... uiobj, selectedlocation='workbench.custom-toolbar') >>> sorted(tools.keys()) ['explore_wd', 'update_to_tip'] >>> toollist ['update_to_tip', '|', 'explore_wd'] >>> tools, toollist = tortoisehgtools( ... uiobj, selectedlocation='workbench.revdetails.custom-menu') >>> sorted(tools.keys()) ['update_to_null', 'update_to_tip'] >>> toollist ['update_to_tip', 'update_to_null'] Valid "locations lists" are: - workbench.custom-toolbar - workbench.revdetails.custom-menu >>> tortoisehgtools(uiobj, selectedlocation='invalid.location') Traceback (most recent call last): ... ValueError: invalid location 'invalid.location' This function can take a ui object or a config object as its input. >>> cfg = config.config() >>> cfg.parse('', hgrctext) >>> tools, toollist = tortoisehgtools(cfg) >>> pprint(tools) #doctest: +NORMALIZE_WHITESPACE {'update_to_tip': {'command': 'hg update tip', 'icon': 'hg-update', 'tooltip': 'Update to tip'}} >>> toollist ['update_to_tip'] >>> cfg = config.config() >>> cfg.parse('', hgrctext_full) >>> tools, toollist = tortoisehgtools( ... cfg, selectedlocation='workbench.custom-toolbar') >>> sorted(tools.keys()) ['explore_wd', 'update_to_tip'] >>> toollist ['update_to_tip', '|', 'explore_wd'] No error for empty config: >>> emptycfg = config.config() >>> tortoisehgtools(emptycfg) ({}, []) >>> tortoisehgtools(emptycfg, selectedlocation='workbench.custom-toolbar') ({}, []) """ if isinstance(uiorconfig, ui.ui): configitems = uiorconfig.configitems configlist = uiorconfig.configlist else: configitems = uiorconfig.items def configlist(section, name): return uiorconfig.get(section, name, '').split() tools = {} for key, value in configitems('tortoisehg-tools'): toolname, field = key.split('.') if toolname not in tools: tools[toolname] = {} bvalue = util.parsebool(value) if bvalue is not None: value = bvalue tools[toolname][field] = value if selectedlocation is None: return tools, sorted(tools.keys()) # Only return the tools that are linked to the selected location if selectedlocation not in dict(tortoisehgtoollocations): raise ValueError('invalid location %r' % selectedlocation) guidef = configlist('tortoisehg', selectedlocation) or [] toollist = [] selectedtools = {} for name in guidef: if name != '|': info = tools.get(name, None) if info is None: continue selectedtools[name] = info toollist.append(name) return selectedtools, toollist def displaytime(date): return util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2') def utctime(date): return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(date[0])) agescales = [ ((lambda n: ngettext("%d year", "%d years", n)), 3600 * 24 * 365), ((lambda n: ngettext("%d month", "%d months", n)), 3600 * 24 * 30), ((lambda n: ngettext("%d week", "%d weeks", n)), 3600 * 24 * 7), ((lambda n: ngettext("%d day", "%d days", n)), 3600 * 24), ((lambda n: ngettext("%d hour", "%d hours", n)), 3600), ((lambda n: ngettext("%d minute", "%d minutes", n)), 60), ((lambda n: ngettext("%d second", "%d seconds", n)), 1), ] def age(date): '''turn a (timestamp, tzoff) tuple into an age string.''' # This is i18n-ed version of mercurial.templatefilters.age(). now = time.time() then = date[0] if then > now: return _('in the future') delta = int(now - then) if delta == 0: return _('now') if delta > agescales[0][1] * 2: return util.shortdate(date) for t, s in agescales: n = delta // s if n >= 2 or s == 1: return t(n) % n def username(user): author = templatefilters.person(user) if not author: author = util.shortuser(user) return author def user(ctx): ''' Get the username of the change context. Does not abort and just returns an empty string if ctx is a working context and no username has been set in mercurial's config. ''' try: user = ctx.user() except error.Abort: if ctx._rev is not None: raise # ctx is a working context and probably no username has # been configured in mercurial's config user = '' return user def get_revision_desc(fctx, curpath=None): """return the revision description as a string""" author = tounicode(username(fctx.user())) rev = fctx.linkrev() # If the source path matches the current path, don't bother including it. if curpath and curpath == fctx.path(): source = u'' else: source = u'(%s)' % tounicode(fctx.path()) date = tounicode(age(fctx.date())) l = tounicode(fctx.description()).splitlines() summary = l and l[0] or '' return u'%s@%s%s:%s "%s"' % (author, rev, source, date, summary) def longsummary(description, limit=None): summary = tounicode(description) lines = summary.splitlines() if not lines: return '' summary = lines[0].strip() add_ellipsis = False if limit: for raw_line in lines[1:]: if len(summary) >= limit: break line = raw_line.strip().replace('\t', ' ') if line: summary += u' ' + line if len(summary) > limit: add_ellipsis = True summary = summary[0:limit] elif len(lines) > 1: add_ellipsis = True if add_ellipsis: summary += u' \u2026' # ellipsis ... return summary def getDeepestSubrepoContainingFile(wfile, ctx): """ Given a filename and context, get the deepest subrepo that contains the file Also return the corresponding subrepo context and the filename relative to its containing subrepo """ if wfile in ctx: return '', wfile, ctx for wsub in ctx.substate: if wfile.startswith(wsub): srev = ctx.substate[wsub][1] stype = ctx.substate[wsub][2] if stype != 'hg': continue if not os.path.exists(ctx._repo.wjoin(wsub)): # Maybe the repository does not exist in the working copy? continue try: sctx = ctx.sub(wsub)._repo[srev] except: # The selected revision does not exist in the working copy continue wfileinsub = wfile[len(wsub)+1:] if wfileinsub in sctx.substate or wfileinsub in sctx: return wsub, wfileinsub, sctx else: wsubsub, wfileinsub, sctx = \ getDeepestSubrepoContainingFile(wfileinsub, sctx) if wsubsub is None: return None, wfile, ctx else: return os.path.join(wsub, wsubsub), wfileinsub, sctx return None, wfile, ctx def getLineSeparator(line): """Get the line separator used on a given line""" # By default assume the default OS line separator linesep = os.linesep lineseptypes = ['\r\n', '\n', '\r'] for sep in lineseptypes: if line.endswith(sep): linesep = sep break return linesep def dispatch(ui, args): req = hgdispatch.request(args, ui) # since hg 2.8 (09573ad59f7b), --config is parsed prior to _dispatch() hgdispatch._parseconfig(req.ui, hgdispatch._earlygetopt(['--config'], req.args)) return hgdispatch._dispatch(req) def buildcmdargs(name, *args, **opts): r"""Build list of command-line arguments >>> buildcmdargs('push', branch='foo') ['push', '--branch', 'foo'] >>> buildcmdargs('graft', r=['0', '1']) ['graft', '-r', '0', '-r', '1'] >>> buildcmdargs('log', no_merges=True, quiet=False, limit=None) ['log', '--no-merges'] >>> buildcmdargs('commit', user='') ['commit', '--user', ''] positional arguments: >>> buildcmdargs('add', 'foo', 'bar') ['add', 'foo', 'bar'] >>> buildcmdargs('cat', '-foo', rev='0') ['cat', '--rev', '0', '--', '-foo'] >>> buildcmdargs('qpush', None) ['qpush'] >>> buildcmdargs('update', '') ['update', ''] type conversion to string: >>> from PyQt4.QtCore import QString >>> buildcmdargs('email', r=[0, 1]) ['email', '-r', '0', '-r', '1'] >>> buildcmdargs('grep', 'foo', rev=2) ['grep', '--rev', '2', 'foo'] >>> buildcmdargs('tag', u'\xc0', message=u'\xc1') ['tag', '--message', u'\xc1', u'\xc0'] >>> buildcmdargs(QString('tag'), QString(u'\xc0'), message=QString(u'\xc1')) [u'tag', '--message', u'\xc1', u'\xc0'] """ stringfy = '%s'.__mod__ # (unicode, QString) -> unicode, otherwise -> str fullargs = [stringfy(name)] for k, v in opts.iteritems(): if v is None: continue if len(k) == 1: aname = '-%s' % k else: aname = '--%s' % k.replace('_', '-') if isinstance(v, bool): if v: fullargs.append(aname) elif isinstance(v, list): for e in v: fullargs.append(aname) fullargs.append(stringfy(e)) else: fullargs.append(aname) fullargs.append(stringfy(v)) args = [stringfy(v) for v in args if v is not None] if util.any(e.startswith('-') for e in args): fullargs.append('--') fullargs.extend(args) return fullargs tortoisehg-2.10/tortoisehg/util/colormap.py0000644000076400007640000000723212231647662020240 0ustar stevesteve# colormap.py - color scheme for annotation # # Copyright (C) 2005 Dan Loda # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import sys, math def _days(ctx, now): return (now - ctx.date()[0]) / (24 * 60 * 60) def _rescale(val, step): return float(step) * int(val / step) def _rescaleceil(val, step): return float(step) * math.ceil(float(val) / step) class AnnotateColorSaturation(object): def __init__(self, maxhues=None, maxsaturations=None): self._maxhues = maxhues self._maxsaturations = maxsaturations def hue(self, angle): return tuple([self.v(angle, r) for r in (0, 120, 240)]) @staticmethod def ang(angle, rotation): angle += rotation angle = angle % 360 if angle > 180: angle = 180 - (angle - 180) return abs(angle) def v(self, angle, rotation): ang = self.ang(angle, rotation) if ang < 60: return 1 elif ang > 120: return 0 else: return 1 - ((ang - 60) / 60) def saturate_v(self, saturation, hv): return int(255 - (saturation/3*(1-hv))) def committer_angle(self, committer): angle = float(abs(hash(committer))) / sys.maxint * 360.0 if self._maxhues is None: return angle return _rescale(angle, 360.0 / self._maxhues) def get_color(self, ctx, now): days = max(_days(ctx, now), 0.0) saturation = 255/((days/50) + 1) if self._maxsaturations: saturation = _rescaleceil(saturation, 255. / self._maxsaturations) hue = self.hue(self.committer_angle(ctx.user())) color = tuple([self.saturate_v(saturation, h) for h in hue]) return "#%x%x%x" % color def makeannotatepalette(fctxs, now, maxcolors, maxhues=None, maxsaturations=None, mindate=None): """Assign limited number of colors for annotation :fctxs: list of filecontexts by lines :now: latest time which will have most significat color :maxcolors: max number of colors :maxhues: max number of committer angles (hues) :maxsaturations: max number of saturations by age :mindate: reassign palette until it includes fctx of mindate (requires maxsaturations) This returns dict of {color: fctxs, ...}. """ if mindate is not None and maxsaturations is None: raise ValueError('mindate must be specified with maxsaturations') sortedfctxs = list(sorted(set(fctxs), key=lambda fctx: -fctx.date()[0])) return _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues, maxsaturations, mindate)[0] def _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues, maxsaturations, mindate): cm = AnnotateColorSaturation(maxhues=maxhues, maxsaturations=maxsaturations) palette = {} def reassignifneeded(fctx): # fctx is the latest fctx which is NOT included in the palette if mindate is None or fctx.date()[0] < mindate or maxsaturations <= 1: return palette, cm return _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues, maxsaturations - 1, mindate) # assign from the latest for maximum discrimination for fctx in sortedfctxs: color = cm.get_color(fctx, now) if color not in palette: if len(palette) >= maxcolors: return reassignifneeded(fctx) palette[color] = [] palette[color].append(fctx) return palette, cm # return cm for debbugging tortoisehg-2.10/tortoisehg/util/obsoleteutil.py0000644000076400007640000000501312170335562021124 0ustar stevesteve# obsolete related util functions (taken from hgview) # # The functions in this file have been taken from hgview's util.py file # (http://hg.logilab.org/review/hgview/file/default/hgviewlib/util.py) # # Copyright (C) 2009-2012 Logilab. All rights reserved. # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. def precursorsmarkers(obsstore, node): return obsstore.precursors.get(node, ()) def successorsmarkers(obsstore, node): return obsstore.successors.get(node, ()) def first_known_precursors(ctx, excluded=()): obsstore = getattr(ctx._repo, 'obsstore', None) startnode = ctx.node() nm = ctx._repo.changelog.nodemap if obsstore is not None: markers = precursorsmarkers(obsstore, startnode) # consider all precursors candidates = set(mark[0] for mark in markers) seen = set(candidates) if startnode in candidates: candidates.remove(startnode) else: seen.add(startnode) while candidates: current = candidates.pop() # is this changeset in the displayed set ? crev = nm.get(current) if crev is not None and crev not in excluded: yield ctx._repo[crev] else: for mark in precursorsmarkers(obsstore, current): if mark[0] not in seen: candidates.add(mark[0]) seen.add(mark[0]) def first_known_successors(ctx, excluded=()): obsstore = getattr(ctx._repo, 'obsstore', None) startnode = ctx.node() nm = ctx._repo.changelog.nodemap if obsstore is not None: markers = successorsmarkers(obsstore, startnode) # consider all precursors candidates = set() for mark in markers: candidates.update(mark[1]) seen = set(candidates) if startnode in candidates: candidates.remove(startnode) else: seen.add(startnode) while candidates: current = candidates.pop() # is this changeset in the displayed set ? crev = nm.get(current) if crev is not None and crev not in excluded: yield ctx._repo[crev] else: for mark in successorsmarkers(obsstore, current): for succ in mark[1]: if succ not in seen: candidates.add(succ) seen.add(succ) tortoisehg-2.10/tortoisehg/util/patchctx.py0000644000076400007640000002013212231647662020234 0ustar stevesteve# patchctx.py - TortoiseHg patch context class # # Copyright 2011 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import sys import shlex import binascii import cStringIO from mercurial import patch, util, error from mercurial import node from mercurial.util import propertycache from hgext import mq, record from tortoisehg.util import hglib class patchctx(object): _parseErrorFileName = '*ParseError*' def __init__(self, patchpath, repo, pf=None, rev=None): """ Read patch context from file :param pf: currently ignored The provided handle is used to read the patch and the patchpath contains the name of the patch. The handle is NOT closed. """ self._path = patchpath self._patchname = os.path.basename(patchpath) self._repo = repo self._rev = rev or 'patch' self._status = [[], [], []] self._fileorder = [] self._user = '' self._desc = '' self._branch = '' self._node = node.nullid self._identity = node.nullid self._mtime = None self._fsize = 0 self._parseerror = None self._phase = 'draft' try: self._mtime = os.path.getmtime(patchpath) self._fsize = os.path.getsize(patchpath) ph = mq.patchheader(self._path) self._ph = ph hash = util.sha1(self._path) hash.update(str(self._mtime)) self._identity = hash.digest() except EnvironmentError: self._date = util.makedate() return try: self._branch = ph.branch or '' self._node = binascii.unhexlify(ph.nodeid) if self._repo.ui.configbool('mq', 'secret'): self._phase = 'secret' except TypeError: pass except AttributeError: # hacks to try to deal with older versions of mq.py self._branch = '' ph.diffstartline = len(ph.comments) if ph.message: ph.diffstartline += 1 except error.ConfigError: pass self._user = ph.user or '' self._desc = ph.message and '\n'.join(ph.message).strip() or '' try: self._date = ph.date and util.parsedate(ph.date) or util.makedate() except error.Abort: self._date = util.makedate() def invalidate(self): # ensure the patch contents are re-read self._mtime = 0 @property def substate(self): return {} # unapplied patch won't include .hgsubstate # unlike changectx, `k in pctx` and `iter(pctx)` just iterates files # included in the patch file, because it does not know the full manifest. def __contains__(self, key): return key in self._files def __iter__(self): return iter(sorted(self._files)) def __str__(self): return node.short(self.node()) def node(self): return self._node def files(self): return self._files.keys() def rev(self): return self._rev def hex(self): return node.hex(self.node()) def user(self): return self._user def date(self): return self._date def description(self): return self._desc def branch(self): return self._branch def parents(self): return () def tags(self): return () def bookmarks(self): return () def children(self): return () def extra(self): return {} def p1(self): return None def p2(self): return None def obsolete(self): return False def extinct(self): return False def unstable(self): return False def bumped(self): return False def divergent(self): return False def troubled(self): return False def troubles(self): return [] def flags(self, wfile): if wfile == self._parseErrorFileName: return '' if wfile in self._files: for gp in patch.readgitpatch(self._files[wfile][0].header): if gp.mode: islink, isexec = gp.mode if islink: return 'l' elif wfile in self._status[1]: # Do not report exec mode change if file is added return '' elif isexec: return 'x' else: # techincally, this case could mean the file has had its # exec bit cleared OR its symlink state removed # TODO: change readgitpatch() to differentiate return '-' return '' # TortoiseHg methods def thgtags(self): return [] def thgwdparent(self): return False def thgmqappliedpatch(self): return False def thgmqpatchname(self): return self._patchname def thgbranchhead(self): return False def thgmqunappliedpatch(self): return True def thgid(self): return self._identity # largefiles/kbfiles methods def hasStandin(self, file): return False def isStandin(self, path): return False def removeStandin(self, path): return path def longsummary(self): if self._repo.ui.configbool('tortoisehg', 'longsummary'): limit = 80 else: limit = None return hglib.longsummary(self.description(), limit) def changesToParent(self, whichparent): 'called by filelistmodel to get list of files' if whichparent == 0 and self._files: return self._status else: return [], [], [] def thgmqoriginalparent(self): '''The revision id of the original patch parent''' return self._ph.parent def thgmqpatchdata(self, wfile): 'called by fileview to get diff data' if wfile == self._parseErrorFileName: return '\n\n\nErrors while parsing patch:\n'+str(self._parseerror) if wfile in self._files: buf = cStringIO.StringIO() for chunk in self._files[wfile]: chunk.write(buf) return buf.getvalue() return '' def phasestr(self): return self._phase def hidden(self): return False @propertycache def _files(self): if not hasattr(self, '_ph') or not self._ph.haspatch: return {} M, A, R = 0, 1, 2 def get_path(a, b): type = (a == '/dev/null') and A or M type = (b == '/dev/null') and R or type rawpath = (b != '/dev/null') and b or a if not (rawpath.startswith('a/') or rawpath.startswith('b/')): return type, rawpath return type, rawpath.split('/', 1)[-1] files = {} pf = open(self._path, 'rb') try: try: # consume comments and headers for i in range(self._ph.diffstartline): pf.readline() for chunk in record.parsepatch(pf): if not isinstance(chunk, record.header): continue top = patch.parsefilename(chunk.header[-2]) bot = patch.parsefilename(chunk.header[-1]) type, path = get_path(top, bot) if path not in chunk.files(): type, path = 0, chunk.files()[-1] if path not in files: self._status[type].append(path) files[path] = [chunk] self._fileorder.append(path) files[path].extend(chunk.hunks) except (patch.PatchError, AttributeError), e: self._status[2].append(self._parseErrorFileName) files[self._parseErrorFileName] = [] self._parseerror = e if 'THGDEBUG' in os.environ: print e finally: pf.close() return files tortoisehg-2.10/tortoisehg/util/bugtraq.py0000644000076400007640000001777112110205646020066 0ustar stevestevefrom ctypes import * import comtypes import pythoncom from comtypes import IUnknown, GUID, COMMETHOD, POINTER, COMError from comtypes.typeinfo import ITypeInfo from comtypes.client import CreateObject from comtypes.automation import _midlSAFEARRAY from _winreg import * from tortoisehg.hgqt import qtlib from tortoisehg.hgqt.i18n import _ class IBugTraqProvider(IUnknown): _iid_ = GUID("{298B927C-7220-423C-B7B4-6E241F00CD93}") _methods_ = [ COMMETHOD([], HRESULT, "ValidateParameters", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['out', 'retval'], POINTER(comtypes.c_int16), "pRetVal") ), COMMETHOD([], HRESULT, "GetLinkText", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ), COMMETHOD([], HRESULT, "GetCommitMessage", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['in'], comtypes.BSTR, "commonRoot"), (['in'], _midlSAFEARRAY(comtypes.BSTR), "pathList"), (['in'], comtypes.BSTR, "originalMessage"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ) ] class IBugTraqProvider2(IBugTraqProvider): _iid_ = GUID("{C5C85E31-2F9B-4916-A7BA-8E27D481EE83}") _methods_ = [ COMMETHOD([], HRESULT, "GetCommitMessage2", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['in'], comtypes.BSTR, "commonURL"), (['in'], comtypes.BSTR, "commonRoot"), (['in'], _midlSAFEARRAY(comtypes.BSTR), "pathList"), (['in'], comtypes.BSTR, "originalMessage"), (['in'], comtypes.BSTR, "bugID"), (['out'], POINTER(comtypes.BSTR), "bugIDOut"), (['out'], POINTER(_midlSAFEARRAY(comtypes.BSTR)), "revPropNames"), (['out'], POINTER(_midlSAFEARRAY(comtypes.BSTR)), "revPropValues"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ), COMMETHOD([], HRESULT, "CheckCommit", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['in'], comtypes.BSTR, "commonURL"), (['in'], comtypes.BSTR, "commonRoot"), (['in'], _midlSAFEARRAY(comtypes.BSTR), "pathList"), (['in'], comtypes.BSTR, "commitMessage"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ), COMMETHOD([], HRESULT, "OnCommitFinished", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "commonRoot"), (['in'], _midlSAFEARRAY(comtypes.BSTR), "pathList"), (['in'], comtypes.BSTR, "logMessage"), (['in'], comtypes.c_long, "revision"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ), COMMETHOD([], HRESULT, "HasOptions", (['out', 'retval'], POINTER(comtypes.c_int16), "pRetVal") ), COMMETHOD([], HRESULT, "ShowOptionsDialog", (['in'], comtypes.c_long, "hParentWnd"), (['in'], comtypes.BSTR, "parameters"), (['out', 'retval'], POINTER(comtypes.BSTR), "pRetVal") ) ] class BugTraq: #svnjiraguid = "{CF732FD7-AA8A-4E9D-9E15-025E4D1A7E9D}" def __init__(self, guid): self.guid = guid self.bugtr = None self.errorshown = False # do not show the COM Error more than once def _get_bugtraq_object(self): if self.bugtr == None: obj = CreateObject(self.guid) try: self.bugtr = obj.QueryInterface(IBugTraqProvider2) except COMError: if not self.errorshown: self.errorshown = True qtlib.ErrorMsgBox(_('Issue Tracker Plugin Error'), _('Could not instantiate Issue Tracker plugin COM object'), _('This error will not be shown again until you restart the workbench')) return None return self.bugtr def get_commit_message(self, parameters, logmessage): commonurl = "" commonroot = "" bugid = "" bstrarray = _midlSAFEARRAY(comtypes.BSTR) pathlist = bstrarray.from_param(()) bugtr = self._get_bugtraq_object() if bugtr is None: return "" try: if self.supports_bugtraq2_interface(): (bugid, revPropNames, revPropValues, newmessage) = bugtr.GetCommitMessage2( 0, parameters, commonurl, commonroot, pathlist, logmessage, bugid) else: newmessage = bugtr.GetCommitMessage( 0, parameters, commonroot, pathlist, logmessage) except COMError: qtlib.ErrorMsgBox(_('Issue Tracker Plugin Error'), _('Error getting commit message information from Issue Tracker plugin')) return "" return newmessage def on_commit_finished(self, logmessage): if not self.supports_bugtraq2_interface(): return "" commonroot = "" bstrarray = _midlSAFEARRAY(comtypes.BSTR) pathlist = bstrarray.from_param(()) bugtr = self._get_bugtraq_object() if bugtr is None: return "" try: errormessage = bugtr.OnCommitFinished(0, commonroot, pathlist, logmessage, 0) except COMError: qtlib.ErrorMsgBox(_('Issue Tracker Plugin Error'), _('Error executing "commit finished" trigger')) return "" return errormessage def show_options_dialog(self, options): if not self.has_options(): return "" bugtr = self._get_bugtraq_object() if bugtr is None: return "" try: options = bugtr.ShowOptionsDialog(0, options) except COMError: qtlib.ErrorMsgBox(_('Issue Tracker Plugin Error'), _('Cannot open Plugin Options dialog')) return "" return options def has_options(self): if not self.supports_bugtraq2_interface(): return False bugtr = self._get_bugtraq_object() if bugtr is None: return False return bugtr.HasOptions() != 0 def get_link_text(self, parameters): bugtr = self._get_bugtraq_object() if bugtr is None: return "" return bugtr.GetLinkText(0, parameters) def supports_bugtraq2_interface(self): bugtr = self._get_bugtraq_object() try: bugtr.HasOptions() return True except (ValueError, AttributeError): return False def get_issue_plugins(): cm = pythoncom.CoCreateInstance(pythoncom.CLSID_StdComponentCategoriesMgr, None, pythoncom.CLSCTX_INPROC,pythoncom.IID_ICatInformation) CATID_BugTraqProvider = pythoncom.MakeIID( "{3494FA92-B139-4730-9591-01135D5E7831}") ret = [] enumerator = cm.EnumClassesOfCategories((CATID_BugTraqProvider,),()) while 1: try: clsid = enumerator.Next() if clsid == (): break except pythoncom.com_error: break ret.extend(clsid) return ret def get_plugin_name(clsid): key = OpenKey(HKEY_CLASSES_ROOT, r"CLSID\%s" % clsid) try: keyvalue = QueryValueEx(key, None)[0] except WindowsError: keyvalue = None key.Close() return keyvalue def get_issue_plugins_with_names(): pluginclsids = get_issue_plugins() keyandnames = [(key, get_plugin_name(key)) for key in pluginclsids] return [kn for kn in keyandnames if kn[1] is not None] tortoisehg-2.10/tortoisehg/util/terminal.py0000644000076400007640000000700412135406415020224 0ustar stevesteveimport os, sys from mercurial import util, ui def defaultshell(): if sys.platform == 'darwin': shell = None # Terminal.App does not support open-to-folder elif os.name == 'nt': shell = 'cmd.exe /K title %(reponame)s' else: shell = 'xterm -T "%(reponame)s"' return shell _defaultshell = defaultshell() def _getplatformexecutablekey(): if sys.platform == 'darwin': key = 'executable-osx' elif os.name == 'nt': key = 'executable-win' else: key = 'executable-unix' return key _platformexecutablekey = _getplatformexecutablekey() def _toolstr(ui, tool, part, default=""): return ui.config("terminal-tools", tool + "." + part, default) toolcache = {} def _findtool(ui, tool): global toolcache if tool in toolcache: return toolcache[tool] for kn in ("regkey", "regkeyalt"): k = _toolstr(ui, tool, kn) if not k: continue p = util.lookupreg(k, _toolstr(ui, tool, "regname")) if p: p = util.findexe(p + _toolstr(ui, tool, "regappend")) if p: toolcache[tool] = p return p global _platformexecutablekey exe = _toolstr(ui, tool, _platformexecutablekey) if not exe: exe = _toolstr(ui, tool, 'executable', tool) path = util.findexe(util.expandpath(exe)) if path: toolcache[tool] = path return path elif tool != exe: path = util.findexe(tool) toolcache[tool] = path return path toolcache[tool] = None return None def _findterminal(ui): '''returns tuple of terminal name and terminal path. tools matched by pattern are returned as (name, toolpath) tools detected by search are returned as (name, toolpath) tortoisehg.shell is returned as (None, tortoisehg.shell) So first return value is an [terminal-tool] name or None and second return value is a toolpath or user configured command line ''' # first check for tool specified in terminal-tools tools = {} for k, v in ui.configitems("terminal-tools"): t = k.split('.')[0] if t not in tools: try: priority = int(_toolstr(ui, t, "priority", "0")) except ValueError, e: priority = -100 tools[t] = priority names = tools.keys() tools = sorted([(-p, t) for t, p in tools.items()]) terminal = ui.config('tortoisehg', 'shell') if terminal: if terminal not in names: # if tortoisehg.terminal does not match an terminal-tools entry, take # the value directly return (None, terminal) # else select this terminal as highest priority (may still use another if # it is not found on this machine) tools.insert(0, (None, terminal)) for p, t in tools: toolpath = _findtool(ui, t) if toolpath: return (t, util.shellquote(toolpath)) # fallback to the default shell global _defaultshell return (None, _defaultshell) def detectterminal(ui_): 'returns tuple of terminal tool path and arguments' if ui_ is None: ui_ = ui.ui() name, pathorconfig = _findterminal(ui_) if name is None: return (pathorconfig, None) else: args = _toolstr(ui_, name, "args") return (pathorconfig, args) def findterminals(ui): seen = set() for key, value in ui.configitems('terminal-tools'): t = key.split('.')[0] seen.add(t) return [t for t in seen if _findtool(ui, t)] tortoisehg-2.10/tortoisehg/util/debugthg.py0000664000076400007640000000272112100577421020203 0ustar stevesteve# debugthg.py - debugging library for TortoiseHg shell extensions # # Copyright 2008 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. debugging = '' try: import _winreg try: hkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, r"Software\TortoiseHg", 0, _winreg.KEY_ALL_ACCESS) val = _winreg.QueryValueEx(hkey, 'OverlayDebug')[0] if val in ('1', 'True'): debugging += 'O' val = _winreg.QueryValueEx(hkey, 'ContextMenuDebug')[0] if val in ('1', 'True'): debugging += 'M' if debugging: import win32traceutil except EnvironmentError: pass except ImportError: import os debugging = os.environ.get("DEBUG_THG", "") if debugging.lower() in ("1", "true"): debugging = True def debugf_No(str, args=None, level=''): pass if debugging: def debug(level=''): return debugging == True or level in debugging def debugf(str, args=None, level=''): if not debug(level): return if args: print str % args elif debug('e') and isinstance(str, BaseException): import traceback traceback.print_exc() else: print str else: def debug(level=''): return False debugf = debugf_No tortoisehg-2.10/tortoisehg/util/hgversion.py0000644000076400007640000000211712231647662020425 0ustar stevesteve# hgversion.py - Version information for Mercurial # # Copyright 2009 Steve Borho # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. import re try: # post 1.1.2 from mercurial import util hgversion = util.version() except AttributeError: # <= 1.1.2 from mercurial import version hgversion = version.get_version() def checkhgversion(v): """range check the Mercurial version""" reqver = ['2', '7'] v = v.split('+')[0] if not v or v == 'unknown' or len(v) >= 12: # can't make any intelligent decisions about unknown or hashes return vers = re.split(r'\.|-', v)[:2] if vers == reqver or len(vers) < 2: return nextver = map(str, divmod(int(reqver[0]) * 10 + int(reqver[1]) + 1, 10)) if vers == nextver: return return (('This version of TortoiseHg requires Mercurial ' 'version %s.n to %s.n, but found %s') % ('.'.join(reqver), '.'.join(nextver), v)) tortoisehg-2.10/tortoisehg/util/gpg.py0000644000076400007640000000166512170335562017200 0ustar stevesteve# gpg.py - TortoiseHg GnuPG support # # Copyright 2013 Elson Wei # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os if os.name == 'nt': import _winreg def findgpg(ui): path = [] for key in (r"Software\GNU\GnuPG", r"Software\Wow6432Node\GNU\GnuPG"): try: hkey = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, key) pfx = _winreg.QueryValueEx(hkey, 'Install Directory')[0] for dirPath, dirNames, fileNames in os.walk(pfx): for f in fileNames: if f == 'gpg.exe': path.append(os.path.join(dirPath, f)) except WindowsError: pass except EnvironmentError: pass return path else: def findgpg(ui): return [] tortoisehg-2.10/PKG-INFO0000644000076400007640000000035712235634575014007 0ustar stevesteveMetadata-Version: 1.0 Name: tortoisehg Version: 2.10 Summary: TortoiseHg dialogs for Mercurial VCS Home-page: http://tortoisehg.org Author: Steve Borho Author-email: steve@borho.org License: GNU GPL2 Description: UNKNOWN Platform: UNKNOWN tortoisehg-2.10/setup.py0000644000076400007640000005062412212222646014411 0ustar stevesteve# setup.py # A distutils setup script to install TortoiseHg in Windows and Posix # environments. # # On Windows, this script is mostly used to build a stand-alone # TortoiseHg package. See installer\build.txt for details. The other # use is to report the current version of the TortoiseHg source. import time import sys import os import shutil import subprocess import cgi import tempfile import re import tarfile from fnmatch import fnmatch from distutils import log from distutils.core import setup, Command from distutils.command.build import build as _build_orig from distutils.command.clean import clean as _clean_orig from distutils.dep_util import newer, newer_group from distutils.spawn import spawn, find_executable from os.path import isdir, exists, join, walk, splitext from i18n.msgfmt import Msgfmt thgcopyright = 'Copyright (C) 2010-2013 Steve Borho and others' hgcopyright = 'Copyright (C) 2005-2013 Matt Mackall and others' class build_mo(Command): description = "build translations (.mo files)" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): podir = 'i18n/tortoisehg' if not os.path.isdir(podir): self.warn("could not find %s/ directory" % podir) return join = os.path.join for po in os.listdir(podir): if not po.endswith('.po'): continue pofile = join(podir, po) modir = join('locale', po[:-3], 'LC_MESSAGES') mofile = join(modir, 'tortoisehg.mo') modata = Msgfmt(pofile).get() self.mkpath(modir) open(mofile, "wb").write(modata) class import_po(Command): description = "import translations (.po file)" user_options = [("package=", "p", "launchpad export package or bzr repo " "[defualt: launchpad-export.tar.gz]"), ("lang=", "l", "languages to be imported, separated by ','") ] def initialize_options(self): self.package = None self.lang = None def finalize_options(self): if not self.package: self.package = 'launchpad-export.tar.gz' if self.lang: self.lang = self.lang.upper().split(',') def _untar(self, name, path='.'): tar = tarfile.open(name, 'r') path = os.path.abspath(path) for tarinfo in tar.getmembers(): # Extract the safe file only p = os.path.abspath(os.path.join(path, tarinfo.name)) if p.startswith(path): tar.extract(tarinfo, path) tar.close() def run(self): if not find_executable('msgcat'): self.warn("could not find msgcat executable") return dest_prefix = 'i18n/tortoisehg' src_prefix = 'po/tortoisehg' log.info('import from %s' % self.package) if os.path.isdir(self.package): self.bzrrepo = True self.package_path = self.package elif tarfile.is_tarfile(self.package): self.bzrrepo = False self.package_path = tempfile.mkdtemp() self._untar(self.package, self.package_path) else: self.warn('%s is not a valid tranlation package' % self.package) return if self.bzrrepo: filter = r'^([\S]+)\.po$' else: filter = r'^tortoisehg-([\S]+)\.po$' r = re.compile(filter) src_dir = os.path.join(self.package_path, src_prefix) for src_file in os.listdir(src_dir): m = r.match(src_file) if not m: continue # filter the language lang = m.group(1) if self.lang and lang.upper() not in self.lang: continue dest_file = join(dest_prefix, lang) + '.po' msg = 'updating %s...' % dest_file cmd = ['msgcat', '--no-location', '-o', dest_file, os.path.join(src_dir, src_file) ] self.execute(spawn, (cmd,), msg) if not self.bzrrepo: shutil.rmtree(self.package_path) class update_pot(Command): description = "extract translatable strings to tortoisehg.pot" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): if not find_executable('xgettext'): self.warn("could not find xgettext executable, tortoisehg.pot" "won't be built") return dirlist = [ '.', 'contrib', 'contrib/win32', 'tortoisehg', 'tortoisehg/hgqt', 'tortoisehg/util', 'tortoisehg/thgutil/iniparse', ] filelist = [] for pathname in dirlist: if not os.path.exists(pathname): continue for filename in os.listdir(pathname): if filename.endswith('.py'): filelist.append(os.path.join(pathname, filename)) filelist.sort() potfile = 'tortoisehg.pot' cmd = [ 'xgettext', '--package-name', 'TortoiseHg', '--msgid-bugs-address', '', '--copyright-holder', thgcopyright, '--from-code', 'ISO-8859-1', '--keyword=_:1,2c,2t', '--add-comments=i18n:', '-d', '.', '-o', potfile, ] cmd += filelist self.make_file(filelist, potfile, spawn, (cmd,)) class build_qt(Command): description = "build PyQt GUIs (.ui) and resources (.qrc)" user_options = [('force', 'f', 'forcibly compile everything' ' (ignore file timestamps)'), ('frozen', None, 'include resources for frozen exe')] boolean_options = ('force', 'frozen') def initialize_options(self): self.force = None self.frozen = False def finalize_options(self): self.set_undefined_options('build', ('force', 'force')) def compile_ui(self, ui_file, py_file=None): # Search for pyuic4 in python bin dir, then in the $Path. if py_file is None: py_file = splitext(ui_file)[0] + "_ui.py" if not(self.force or newer(ui_file, py_file)): return try: from PyQt4 import uic fp = open(py_file, 'w') uic.compileUi(ui_file, fp) fp.close() log.info('compiled %s into %s' % (ui_file, py_file)) except Exception, e: self.warn('Unable to compile user interface %s: %s' % (py_file, e)) if not exists(py_file) or not file(py_file).read(): raise SystemExit(1) return def compile_rc(self, qrc_file, py_file=None): # Search for pyuic4 in python bin dir, then in the $Path. if py_file is None: py_file = splitext(qrc_file)[0] + "_rc.py" if not(self.force or newer(qrc_file, py_file)): return import PyQt4 origpath = os.getenv('PATH') path = origpath.split(os.pathsep) pyqtfolder = os.path.dirname(PyQt4.__file__) path.append(os.path.join(pyqtfolder, 'bin')) os.putenv('PATH', os.pathsep.join(path)) if os.system('pyrcc4 "%s" -o "%s"' % (qrc_file, py_file)) > 0: self.warn("Unable to generate python module %s for resource file %s" % (py_file, qrc_file)) if not exists(py_file) or not file(py_file).read(): raise SystemExit(1) else: log.info('compiled %s into %s' % (qrc_file, py_file)) os.putenv('PATH', origpath) def _generate_qrc(self, qrc_file, srcfiles, prefix): basedir = os.path.dirname(qrc_file) f = open(qrc_file, 'w') try: f.write('\n') f.write(' \n' % cgi.escape(prefix)) for e in srcfiles: relpath = e[len(basedir) + 1:] f.write(' %s\n' % cgi.escape(relpath.replace(os.path.sep, '/'))) f.write(' \n') f.write('\n') finally: f.close() def build_rc(self, py_file, basedir, prefix='/'): """Generate compiled resource including any files under basedir""" # For details, see http://doc.qt.nokia.com/latest/resources.html qrc_file = os.path.join(basedir, '%s.qrc' % os.path.basename(basedir)) srcfiles = [os.path.join(root, e) for root, _dirs, files in os.walk(basedir) for e in files] # NOTE: Here we cannot detect deleted files. In such case, we need # to remove .qrc manually. if not (self.force or newer_group(srcfiles, py_file)): return try: self._generate_qrc(qrc_file, srcfiles, prefix) self.compile_rc(qrc_file, py_file) finally: os.unlink(qrc_file) def _build_translations(self, basepath): """Build translations_rc.py which inclues qt_xx.qm""" from PyQt4.QtCore import QLibraryInfo trpath = unicode(QLibraryInfo.location(QLibraryInfo.TranslationsPath)) d = tempfile.mkdtemp() try: for e in os.listdir(trpath): if re.match(r'qt_[a-z]{2}(_[A-Z]{2})?\.ts$', e): r = os.system('lrelease "%s" -qm "%s"' % (os.path.join(trpath, e), os.path.join(d, e[:-3] + '.qm'))) if r > 0: self.warn('Unable to generate Qt message file' ' from %s' % e) self.build_rc(os.path.join(basepath, 'translations_rc.py'), d, '/translations') finally: shutil.rmtree(d) def run(self): self._wrapuic() basepath = join(os.path.dirname(__file__), 'tortoisehg', 'hgqt') self.build_rc(os.path.join(basepath, 'icons_rc.py'), os.path.join(os.path.dirname(__file__), 'icons'), '/icons') if self.frozen: self._build_translations(basepath) for dirpath, _, filenames in os.walk(basepath): for filename in filenames: if filename.endswith('.ui'): self.compile_ui(join(dirpath, filename)) elif filename.endswith('.qrc'): self.compile_rc(join(dirpath, filename)) _wrappeduic = False @classmethod def _wrapuic(cls): """wrap uic to use gettext's _() in place of tr()""" if cls._wrappeduic: return from PyQt4.uic.Compiler import compiler, qtproxies, indenter class _UICompiler(compiler.UICompiler): def createToplevelWidget(self, classname, widgetname): o = indenter.getIndenter() o.level = 0 o.write('from tortoisehg.hgqt.i18n import _') return super(_UICompiler, self).createToplevelWidget(classname, widgetname) compiler.UICompiler = _UICompiler class _i18n_string(qtproxies.i18n_string): def __str__(self): return "_('%s')" % self.string.encode('string-escape') qtproxies.i18n_string = _i18n_string cls._wrappeduic = True class clean_local(Command): pats = ['*.py[co]', '*_ui.py', '*_rc.py', '*.mo', '*.orig', '*.rej'] excludedirs = ['.hg', 'build', 'dist'] description = 'clean up generated files (%s)' % ', '.join(pats) user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): for e in self._walkpaths('.'): log.info("removing '%s'" % e) os.remove(e) def _walkpaths(self, path): for root, _dirs, files in os.walk(path): if any(root == join(path, e) or root.startswith(join(path, e, '')) for e in self.excludedirs): continue for e in files: fpath = join(root, e) if any(fnmatch(fpath, p) for p in self.pats): yield fpath class build(_build_orig): sub_commands = [ ('build_qt', None), ('build_mo', None), ] + _build_orig.sub_commands class clean(_clean_orig): sub_commands = [ ('clean_local', None), ] + _clean_orig.sub_commands def run(self): _clean_orig.run(self) for e in self.get_sub_commands(): self.run_command(e) cmdclass = { 'build': build, 'build_qt': build_qt , 'build_mo': build_mo , 'clean': clean, 'clean_local': clean_local, 'update_pot': update_pot , 'import_po': import_po } def setup_windows(version): # Specific definitios for Windows NT-alike installations _scripts = [] _data_files = [] _packages = ['tortoisehg.hgqt', 'tortoisehg.util', 'tortoisehg'] extra = {} hgextmods = [] # py2exe needs to be installed to work try: import py2exe # Help py2exe to find win32com.shell try: import modulefinder import win32com for p in win32com.__path__[1:]: # Take the path to win32comext modulefinder.AddPackagePath("win32com", p) pn = "win32com.shell" __import__(pn) m = sys.modules[pn] for p in m.__path__[1:]: modulefinder.AddPackagePath(pn, p) except ImportError: pass except ImportError: if '--version' not in sys.argv: raise # Allow use of environment variables to specify the location of Mercurial import modulefinder path = os.getenv('MERCURIAL_PATH') if path: modulefinder.AddPackagePath('mercurial', path) path = os.getenv('HGEXT_PATH') if path: modulefinder.AddPackagePath('hgext', path) if 'py2exe' in sys.argv: import hgext hgextdir = os.path.dirname(hgext.__file__) hgextmods = set(["hgext." + os.path.splitext(f)[0] for f in os.listdir(hgextdir)]) _data_files = [(root, [os.path.join(root, file_) for file_ in files]) for root, dirs, files in os.walk('icons')] # for PyQt, see http://www.py2exe.org/index.cgi/Py2exeAndPyQt includes = ['sip'] # Qt4 plugins, see http://stackoverflow.com/questions/2206406/ def qt4_plugins(subdir, *dlls): import PyQt4 pluginsdir = join(os.path.dirname(PyQt4.__file__), 'plugins') return (subdir, [join(pluginsdir, subdir, e) for e in dlls]) _data_files.append(qt4_plugins('imageformats', 'qico4.dll', 'qsvg4.dll')) # Manually include other modules py2exe can't find by itself. if 'hgext.highlight' in hgextmods: includes += ['pygments.*', 'pygments.lexers.*', 'pygments.formatters.*', 'pygments.filters.*', 'pygments.styles.*'] if 'hgext.patchbomb' in hgextmods: includes += ['email.*', 'email.mime.*'] extra['options'] = { "py2exe" : { "skip_archive" : 0, # Don't pull in all this MFC stuff used by the makepy UI. "excludes" : "pywin,pywin.dialogs,pywin.dialogs.list" ",setup,distutils", # required only for in-place use "includes" : includes, "optimize" : 1 } } shutil.copyfile('thg', 'thgw') extra['console'] = [ {'script':'thg', 'icon_resources':[(0,'icons/thg_logo.ico')], 'description':'TortoiseHg GUI tools for Mercurial SCM', 'copyright':thgcopyright, 'product_version':version}, {'script':'contrib/hg', 'icon_resources':[(0,'icons/hg.ico')], 'description':'Mercurial Distributed SCM', 'copyright':hgcopyright, 'product_version':version}, {'script':'win32/docdiff.py', 'icon_resources':[(0,'icons/TortoiseMerge.ico')], 'copyright':thgcopyright, 'product_version':version} ] extra['windows'] = [ {'script':'thgw', 'icon_resources':[(0,'icons/thg_logo.ico')], 'description':'TortoiseHg GUI tools for Mercurial SCM', 'copyright':thgcopyright, 'product_version':version}, {'script':'TortoiseHgOverlayServer.py', 'icon_resources':[(0,'icons/thg_logo.ico')], 'description':'TortoiseHg Overlay Icon Server', 'copyright':thgcopyright, 'product_version':version} ] return _scripts, _packages, _data_files, extra def setup_posix(): # Specific definitios for Posix installations _extra = {} _scripts = ['thg'] _packages = ['tortoisehg', 'tortoisehg.hgqt', 'tortoisehg.util'] _data_files = [(os.path.join('share/pixmaps/tortoisehg', root), [os.path.join(root, file_) for file_ in files]) for root, dirs, files in os.walk('icons')] _data_files += [(os.path.join('share', root), [os.path.join(root, file_) for file_ in files]) for root, dirs, files in os.walk('locale')] _data_files += [('/usr/share/nautilus-python/extensions/', ['contrib/nautilus-thg.py'])] # Create a config.py. Distributions will need to supply their own cfgfile = os.path.join('tortoisehg', 'util', 'config.py') if not os.path.exists(cfgfile) and not os.path.exists('.hg/requires'): f = open(cfgfile, "w") f.write('bin_path = "/usr/bin"\n') f.write('license_path = "/usr/share/doc/tortoisehg/Copying.txt.gz"\n') f.write('locale_path = "/usr/share/locale"\n') f.write('icon_path = "/usr/share/pixmaps/tortoisehg/icons"\n') f.write('nofork = True\n') f.close() return _scripts, _packages, _data_files, _extra def runcmd(cmd, env): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) out, err = p.communicate() # If root is executing setup.py, but the repository is owned by # another user (as in "sudo python setup.py install") we will get # trust warnings since the .hg/hgrc file is untrusted. That is # fine, we don't want to load it anyway. err = [e for e in err.splitlines() if not e.startswith('Not trusting file')] if err: return '' return out if __name__ == '__main__': version = '' if os.path.isdir('.hg'): from tortoisehg.util import version as _version branch, version = _version.liveversion() if version.endswith('+'): version += time.strftime('%Y%m%d') elif os.path.exists('.hg_archival.txt'): kw = dict([t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')) if 'tag' in kw: version = kw['tag'] elif 'latesttag' in kw: version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw else: version = kw.get('node', '')[:12] if version: f = open("tortoisehg/util/__version__.py", "w") f.write('# this file is autogenerated by setup.py\n') f.write('version = "%s"\n' % version) f.close() try: import tortoisehg.util.__version__ version = tortoisehg.util.__version__.version except ImportError: version = 'unknown' if os.name == "nt": (scripts, packages, data_files, extra) = setup_windows(version) desc = 'Windows shell extension for Mercurial VCS' # Windows binary file versions for exe/dll files must have the # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535 from tortoisehg.util.version import package_version setupversion = package_version() productname = 'TortoiseHg' else: (scripts, packages, data_files, extra) = setup_posix() desc = 'TortoiseHg dialogs for Mercurial VCS' setupversion = version productname = 'tortoisehg' setup(name=productname, version=setupversion, author='Steve Borho', author_email='steve@borho.org', url='http://tortoisehg.org', description=desc, license='GNU GPL2', scripts=scripts, packages=packages, data_files=data_files, cmdclass=cmdclass, **extra ) tortoisehg-2.10/COPYING.txt0000644000076400007640000004325412110205644014545 0ustar stevesteve GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. tortoisehg-2.10/i18n/0000755000076400007640000000000012235634575013464 5ustar stevestevetortoisehg-2.10/i18n/tortoisehg/0000755000076400007640000000000012235634575015653 5ustar stevestevetortoisehg-2.10/i18n/tortoisehg/tr.po0000644000076400007640000041177112235634452016645 0ustar stevesteve# Turkish translation for tortoisehg # Copyright (c) 2009 Rosetta Contributors and Canonical Ltd 2009 # This file is distributed under the same license as the tortoisehg package. # FIRST AUTHOR , 2009. # msgid "" msgstr "" "Project-Id-Version: tortoisehg\n" "Report-Msgid-Bugs-To: Turkish \n" "POT-Creation-Date: 2013-10-31 00:28-0200\n" "PO-Revision-Date: 2010-10-08 16:52+0000\n" "Last-Translator: can kaçan \n" "Language-Team: Turkish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Launchpad-Export-Date: 2013-10-31 05:28+0000\n" "X-Generator: Launchpad (build 16820)\n" msgid "TortoiseHg Overlay Icon Server" msgstr "" msgid "Exit" msgstr "Çık" msgid "About" msgstr "" msgid "Copyright 2008-2013 Steve Borho and others" msgstr "" msgid "Several icons are courtesy of the TortoiseSVN and Tango projects" msgstr "" msgid "You can visit our site here" msgstr "" msgid "&License" msgstr "" msgid "&Close" msgstr "" #, python-format msgid "version %s" msgstr "" #, python-format msgid "with Mercurial-%s, Python-%s, PyQt-%s, Qt-%s" msgstr "" #, python-format msgid "A new version of TortoiseHg (%s) is ready for download!" msgstr "" msgid "= Working Directory Parent =" msgstr "" msgid "Revision:" msgstr "Değişiklik:" msgid "Only files modified/created in this revision" msgstr "" msgid "Recurse into subrepositories" msgstr "" msgid "Destination path:" msgstr "Hedef yer:" msgid "Browse..." msgstr "Gözat..." msgid "Archive types:" msgstr "Arşiv tipleri:" msgid "Directory of files" msgstr "Dosyaların dizinleri" msgid "Uncompressed tar archive" msgstr "Sıkıştırılmamış tar arşiv" msgid "Tar archive compressed using bzip2" msgstr "bzip2 kullanılarak sıkıştırılmış tar arşiv" msgid "Tar archive compressed using gzip" msgstr "gzip kullanılarak sıkıştırılmış tar arşiv" msgid "Uncompressed zip archive" msgstr "Sıkıştırılmamış zip arşiv" msgid "Zip archive compressed using deflate" msgstr "Deflate kullanılarak sıkıştırılmış zip arşiv" msgid "Hg command:" msgstr "" msgid "Always show output" msgstr "" msgid "&Archive" msgstr "" msgid "&Detail" msgstr "" msgid "Cancel" msgstr "" #, python-format msgid "Archive - %s" msgstr "Arşiv - %s" msgid "Select Destination Folder" msgstr "Hedef Dizini Seç" msgid "Select Destination File" msgstr "Hedef Dosyayı seç" msgid "All files (*)" msgstr "" msgid "Tar archives" msgstr "Tar arşivleri" msgid "Bzip2 tar archives" msgstr "Bzip2 tar arşivleri" msgid "Gzip tar archives" msgstr "Gzip tar arşivleri" msgid "Zip archives" msgstr "" msgid "Duplicate Name" msgstr "" #, python-format msgid "The destination \"%s\" already exists as a file!" msgstr "" msgid "Confirm Overwrite" msgstr "Üzerine Yazılsın" #, python-format msgid "" "The directory \"%s\" is not empty!\n" "\n" "Do you want to overwrite it?" msgstr "" #, python-format msgid "" "The file \"%s\" already exists!\n" "\n" "Do you want to overwrite it?" msgstr "" #, python-format msgid "The destination \"%s\" already exists as a folder!" msgstr "" #, python-format msgid "Backout - %s" msgstr "" msgid "Prepare to backout" msgstr "" msgid "Verify backout revision and ensure your working directory is clean." msgstr "" msgid "Unable to backout" msgstr "" msgid "Backout revision not found" msgstr "" msgid "Backing out a parent revision is a single step operation" msgstr "" msgid "Backout requires a parent revision" msgstr "" msgid "Cannot backout change on a different branch" msgstr "" msgid "Backout revision" msgstr "" msgid "Not a head, backout will create a new head!" msgstr "" msgid "Current local revision" msgstr "" msgid "Merge parent to backout to" msgstr "" msgid "" "To backout a merge revision you must select which parent to backout " "to (i.e. whose changes will be kept)" msgstr "" #, python-format msgid "First Parent: revision %s (%s)" msgstr "" msgid "Backout to the first parent of the merge revision" msgstr "" #, python-format msgid "Second Parent: revision %s (%s)" msgstr "" msgid "Backout to the second parent of the merge revision" msgstr "" msgid "Working directory status" msgstr "" msgid "Checking..." msgstr "" msgid "" "Before backout, you must commit, shelve to patch, or discard changes." msgstr "" msgid "Automatically resolve merge conflicts where possible" msgstr "" msgid "Uncommitted local changes are detected" msgstr "" msgid "Clean" msgstr "" msgid "Backing out, then merging..." msgstr "" msgid "All conflicting files will be marked unresolved." msgstr "" msgid "Automatically advance to next page when backout and merge are complete." msgstr "" #, python-format msgid "" "%d files have merge conflicts that must be resolved" msgstr "" msgid "No merge conflicts, ready to commit" msgstr "" msgid "Commit backout and merge results" msgstr "" msgid "Parents" msgstr "Ebeveynler" msgid "Working Directory" msgstr "" msgid "Working Directory (merged)" msgstr "" msgid "Commit message" msgstr "" msgid "Skip final confirmation page, close after commit." msgstr "" msgid "Backed out merge changeset: " msgstr "" msgid "Backed out changeset: " msgstr "" msgid "Confirm Discard Message" msgstr "" msgid "Discard current backout message?" msgstr "" msgid "Use English backout message" msgstr "" #, python-format msgid "Backed out merge revision to its first parent (%s)" msgstr "" #, python-format msgid "Backed out merge revision to its second parent (%s)" msgstr "" msgid "Backing out and committing..." msgstr "" msgid "Please wait while making backout." msgstr "" msgid "Committing..." msgstr "" msgid "Please wait while committing merged files." msgstr "" msgid "Finished" msgstr "" msgid "Backout changeset" msgstr "" #, python-format msgid "Bisect - %s" msgstr "" msgid "Known good revision:" msgstr "" msgid "Accept" msgstr "" msgid "Known bad revision:" msgstr "" msgid "Revision is Good" msgstr "" msgid "Revision is Bad" msgstr "" msgid "Skip this Revision" msgstr "" msgid "Close" msgstr "Kapat" msgid "Error encountered." msgstr "" msgid "Culprit found." msgstr "" msgid "Revision" msgstr "" msgid "Test this revision and report findings. (good/bad/skip)" msgstr "" #, python-format msgid "%s (hint: %s)" msgstr "" msgid "Bookmark:" msgstr "Yer imi" msgid "New Name:" msgstr "Yeni İsim:" msgid "Activate:" msgstr "" msgid "&Add" msgstr "" msgid "Re&name" msgstr "" msgid "&Remove" msgstr "" msgid "&Move" msgstr "" #, python-format msgid "Bookmark - %s" msgstr "" #, python-format msgid "A bookmark named \"%s\" already exists" msgstr "" #, python-format msgid "Bookmark '%s' has been added" msgstr "" #, python-format msgid "Bookmark named \"%s\" does not exist" msgstr "" #, python-format msgid "Bookmark '%s' has been moved" msgstr "" #, python-format msgid "Bookmark '%s' does not exist" msgstr "'%s' yer imi girilmemiş" #, python-format msgid "Bookmark '%s' has been removed" msgstr "" #, python-format msgid "Bookmark '%s' has been renamed to '%s'" msgstr "" #, python-format msgid "%s - branch operation" msgstr "" msgid "Select branch of merge commit" msgstr "" msgid "Changes take effect on next commit" msgstr "" msgid "No branch changes" msgstr "" msgid "Open a new named branch" msgstr "" msgid "Close current branch" msgstr "" #, python-format msgid "Please report this bug to our bug tracker" msgstr "" msgid "Checking for updates..." msgstr "" msgid "Copy" msgstr "" msgid "Quit" msgstr "" msgid "TortoiseHg Bug Report" msgstr "" msgid "Upgrading to a more recent TortoiseHg is recommended." msgstr "" msgid "Your TortoiseHg is up to date." msgstr "" msgid "Save error report to" msgstr "" msgid "Text files (*.txt)" msgstr "" msgid "Error writing file" msgstr "" msgid "TortoiseHg Error" msgstr "" msgid "" "If you still have trouble, please file a bug report." msgstr "" msgid "Visual Diff" msgstr "" msgid "View file changes in external diff tool" msgstr "" msgid "Edit Local" msgstr "" msgid "Edit current file in working copy" msgstr "" msgid "Revert to Revision" msgstr "" msgid "Revert file(s) to contents at this revision" msgstr "" msgid "Patch failed to apply" msgstr "" msgid "Manually resolve rejected chunks?" msgstr "" msgid "Edit patched file and rejects?" msgstr "" msgid "No deletable chunks" msgstr "" msgid "Completely remove file from patch?" msgstr "" msgid "Revert all file changes?" msgstr "" msgid "No chunks remain" msgstr "" msgid "file has been deleted, refresh" msgstr "" msgid "file has been modified, refresh" msgstr "" msgid "Unable to merge chunks" msgstr "" msgid "Add or remove patches must be merged in the working directory" msgstr "" msgid "Unable to remove" msgstr "" #, python-format msgid "" "Unable to remove file %s,\n" "permission denied" msgstr "" msgctxt "files" msgid "All" msgstr "" msgctxt "files" msgid "None" msgstr "" msgid "Toggle display of text search bar" msgstr "" msgid "Diff Toolbar" msgstr "" #, python-format msgid "Chunks selected: %d / %d" msgstr "" msgid "Please wait while the file is opened ..." msgstr "" msgid "Source:" msgstr "" msgid "Destination:" msgstr "" msgid "Options" msgstr "" msgid "Clone to revision:" msgstr "" msgid "Do not update the new working directory" msgstr "Yeni çalışma dizinini güncelleme" msgid "Use pull protocol to copy metadata" msgstr "" msgid "Use uncompressed transfer" msgstr "Sıkıştırılmamış transferi kullan" msgid "Include patch queue" msgstr "" msgid "Use proxy server" msgstr "Proxy sunucu kullan" msgid "Do not verify host certificate" msgstr "" msgid "Remote command:" msgstr "Uzaktan komut:" msgid "Start revision:" msgstr "" msgid "&Clone" msgstr "" msgid "Detail" msgstr "" #, python-format msgid "Clone - %s" msgstr "" msgid "TortoiseHg Clone" msgstr "" msgid "Error creating destination folder" msgstr "" msgid "Please specify a different path." msgstr "" msgid "Source path is empty" msgstr "Hedef yol boş" msgid "Please enter a valid source path." msgstr "" msgid "Source and destination are the same" msgstr "Kaynak ve hedef aynı" msgid "Please specify different paths." msgstr "" msgid "Please enter a new destination path." msgstr "" msgid "Select source repository" msgstr "" msgid "Select destination repository" msgstr "" msgid "Select patch folder" msgstr "" msgid "failed to start command\n" msgstr "" msgid "error while running command\n" msgstr "" msgid "Terminated by user" msgstr "" #, python-format msgid "[command terminated by user %s]" msgstr "" #, python-format msgid "[command interrupted %s]" msgstr "" #, python-format msgid "[command returned code %d %%s]" msgstr "" #, python-format msgid "[command completed successfully %s]" msgstr "" msgid "Running..." msgstr "" msgid "Failed!" msgstr "" msgid "Clea&r Log" msgstr "" msgid "TortoiseHg Command Dialog" msgstr "" msgid "Confirm Exit" msgstr "Çıkışı Onayla" msgid "" "Mercurial command is still running.\n" "Are you sure you want to terminate?" msgstr "" msgid "Command Error" msgstr "" #, python-format msgid "[Code: %d]" msgstr "" msgctxt "start progress" msgid "Commit" msgstr "" msgctxt "start progress" msgid "MQ Action" msgstr "" msgctxt "start progress" msgid "Rollback" msgstr "" msgid "Commit Dialog Toolbar" msgstr "" msgid "Branch: " msgstr "" msgid "Copy message" msgstr "" msgid "Copy one of the recent commit messages" msgstr "" msgid "Show Issues" msgstr "" msgid "Please wait..." msgstr "" #, python-format msgid "Failed to load issue tracker '%s': %s" msgstr "" msgid "Issue Tracker" msgstr "" msgid "Show Issues..." msgstr "" msgid "Stop" msgstr "" msgid "Commit changes" msgstr "" msgid "Commit" msgstr "Gönder" msgid "Amend current revision" msgstr "" msgid "Amend" msgstr "" msgid "Create a new patch" msgstr "" msgid "QNew" msgstr "QNew" msgid "Refresh current patch" msgstr "" msgid "QRefresh" msgstr "QRefresh" msgid "Confirm Branch Change" msgstr "" #, python-format msgid "Named branch \"%s\" already exists, last used in revision %d\n" msgstr "" msgid "Restart &Branch" msgstr "" msgid "&Commit to current branch" msgstr "" msgid "Confirm New Branch" msgstr "Yeni Dalı Onayla" #, python-format msgid "Create new named branch \"%s\" with this commit?\n" msgstr "" msgid "Create &Branch" msgstr "" msgid "Close Branch: " msgstr "" msgid "New Branch: " msgstr "" #, python-format msgid "Selected Options: %s" msgstr "" msgid "Parent:" msgstr "" msgid "Patch name:" msgstr "" #, python-format msgid "Close %s branch" msgstr "" #, python-format msgid "Rollback commit to revision %d" msgstr "" msgid "Confirm Undo" msgstr "" msgid "Discard current commit message?" msgstr "Şimdiki işleme mesajından vazgeçilsin mi?" msgid "Message Translation Failure" msgstr "" msgid "" "Unable to translate message to local encoding\n" "Consider setting HGENCODING environment variable\n" "Replace untranslatable characters with \"?\"?\n" msgstr "" msgid "&Replace" msgstr "" msgid "Nothing Commited" msgstr "Hiç bir şey işlenmedi" msgid "Please enter commit message" msgstr "Lütfen işleme mesajı giriniz." msgid "" "No issue link was found in the commit message. The commit message should " "contain an issue link. Configure this in the 'Issue Tracking' section of " "the settings." msgstr "" msgid "No files checked" msgstr "" msgid "No modified files checkmarked for commit" msgstr "" msgid "Confirm Add" msgstr "" msgid "Add selected untracked files?" msgstr "" msgid "Confirm Remove" msgstr "" msgid "Remove selected deleted files?" msgstr "" msgctxt "window title" msgid "Commit" msgstr "" #, python-format msgid "%s - commit options" msgstr "" msgid "Set username:" msgstr "" msgid "Save in Repo" msgstr "" msgid "Save Global" msgstr "" msgid "Set Date:" msgstr "" msgid "Update" msgstr "" msgid "Push After Commit:" msgstr "" msgid "Auto Includes:" msgstr "" msgid "Recurse into subrepositories (--subrepos)" msgstr "" msgid "Unable to save username" msgstr "" msgid "Iniparse must be installed." msgstr "" msgid "Unable to write configuration file" msgstr "" msgid "Unable to save after commit push" msgstr "" msgid "Unable to save auto include list" msgstr "" msgid "Unable to save recurse in subrepos." msgstr "" msgid "Invalid date format" msgstr "" msgid "No username configured" msgstr "" #, python-format msgid "%s - commit" msgstr "" msgid "TortoiseHg Commit" msgstr "" msgid "Are you sure that you want to cancel the commit operation?" msgstr "" msgid "Compress changesets up to and including" msgstr "" msgid "Onto destination" msgstr "" msgid "Compress" msgstr "" #, python-format msgid "Compress - %s" msgstr "" msgid "" "Before compress, you must commit or discard changes." msgstr "" msgid "You may continue the compress" msgstr "" msgid "Changes have been moved, you must now commit" msgstr "" msgctxt "action button" msgid "Commit" msgstr "" msgid "Compress is complete, old history untouched" msgstr "" msgid "must be specified repository" msgstr "" msgid "must be specified 'type' in style" msgstr "" msgid "Summary:" msgstr "" msgid "User:" msgstr "" msgid "Date:" msgstr "" msgid "Age:" msgstr "" msgid "Branch:" msgstr "" msgid "Close:" msgstr "" msgid "Tags:" msgstr "" msgid "Graft:" msgstr "" msgid "Transplant:" msgstr "" msgid "Obsolete state:" msgstr "" msgid "Perforce:" msgstr "" msgid "Subversion:" msgstr "" msgid "Converted From:" msgstr "" msgid "Original Parent:" msgstr "" msgid "No items to display" msgstr "" msgid "Use compact view" msgstr "" msgid "Patch:" msgstr "Paket:" #, python-format msgid "Displaying %(count)d of %(total)d items" msgstr "" msgid "Select a GUI location to edit:" msgstr "" msgid "Select the toolbar or menu to change" msgstr "" msgid "Tools shown on selected location" msgstr "" msgid "Delete from list" msgstr "" msgid "Add to list" msgstr "" msgid "Add separator" msgstr "" msgid "List of all tools" msgstr "" msgid "New Tool ..." msgstr "" msgid "Edit Tool ..." msgstr "" msgid "Delete Tool" msgstr "" msgid "Type" msgstr "" msgid "Name" msgstr "" msgid "Command" msgstr "" msgid "New hook" msgstr "" msgid "Edit hook" msgstr "" msgid "Delete hook" msgstr "" msgid "Replace existing hook?" msgstr "" #, python-format msgid "" "There is an existing %s.%s hook.\n" "\n" "Do you want to replace it?" msgstr "" msgid "OK" msgstr "" msgid "Missing information" msgstr "" msgid "All items" msgstr "" msgid "Working directory" msgstr "" msgid "All revisions" msgstr "" msgid "All contexts" msgstr "" msgid "Fixed revisions" msgstr "" msgid "Applied patches" msgstr "" msgid "Applied patches or qparent" msgstr "" msgid "" msgstr "" msgid "Configure Custom Tool" msgstr "" msgid "Tool name" msgstr "" msgid "The tool name. It cannot contain spaces." msgstr "" msgid "" "The command that will be executed.\n" "To execute a Mercurial command use \"hg\" (rather than \"hg.exe\") as the " "executable command.\n" "You can use several {VARIABLES} to compose your command:\n" "- {ROOT}: The path to the current repository root.\n" "- {REV} / {REVID}: the selected revision number / hexadecimal revision id " "hash respectively.\n" "- {FILES}: The list of files touched by the selected revision.\n" "- {ALLFILES}: All the files tracked by Mercurial on the selected revision." msgstr "" msgid "" "The directory where the command will be executed.\n" "If this is not set, the root of the current repository will be used " "instead.\n" "You can use the same {VARIABLES} as on the \"Command\" setting.\n" msgstr "" msgid "Tool label" msgstr "" msgid "" "The tool label, which is what will be shown on the repowidget context menu.\n" "If no label is set, the tool name will be used as the tool label.\n" "If no tooltip is set, the label will be used as the tooltip as well." msgstr "" msgid "Tooltip" msgstr "" msgid "" "The tooltip that will be shown on the tool button.\n" "This is only shown when the tool button is shown on\n" "the workbench toolbar." msgstr "" msgid "Icon" msgstr "" msgid "" "The tool icon.\n" "You can use any built-in TortoiseHg icon\n" "by setting this value to a valid TortoiseHg icon name\n" "(e.g. clone, add, remove, sync, thg-logo, hg-update, etc).\n" "You can also set this value to the absolute path to\n" "any icon on your file system." msgstr "" msgid "On repowidget, show for" msgstr "" msgid "" "For which kinds of revisions the tool will be enabled\n" "It is only taken into account when the tool is shown on the\n" "selected revision context menu." msgstr "" msgid "Show Output Log" msgstr "" msgid "" "When enabled, automatically show the Output Log when the command is run.\n" "Default: False." msgstr "" msgid "You must set a tool name." msgstr "" msgid "The tool name cannot have any spaces in it." msgstr "" msgid "You must set a command to run." msgstr "" msgid "Configure Hook" msgstr "" msgid "Hook type" msgstr "" msgid "Select when your command will be run" msgstr "" msgid "The hook name. It cannot contain spaces." msgstr "" msgid "" "The command that will be executed.\n" "To execute a python function prepend the command with \"python:\".\n" msgstr "" msgid "You must set a valid hook type." msgstr "" msgid "The hook name cannot contain any spaces, tabs or '=' characters." msgstr "" msgid "Custom Tools" msgstr "" #, python-format msgid "command parse error: %s" msgstr "" #, python-format msgid "no matches found: %s" msgstr "" msgid "Output Log" msgstr "" msgid "File &History" msgstr "" msgid "Show the history of the selected file" msgstr "" msgid "Folder &History" msgstr "" msgid "Co&mpare File Revisions" msgstr "" msgid "Compare revisions of the selected file" msgstr "" msgid "&Diff to Parent" msgstr "" msgid "Diff to &Local" msgstr "" msgid "View changes to current in external diff tool" msgstr "" msgid "&View at Revision" msgstr "" msgid "View file as it appeared at this revision" msgstr "" msgid "&Save at Revision..." msgstr "" msgid "Save file as it appeared at this revision" msgstr "" msgid "&Edit Local" msgstr "" msgid "&Open Local" msgstr "" msgid "Copy &Path" msgstr "" msgid "Copy full path of file(s) to the clipboard" msgstr "" msgid "&Revert to Revision..." msgstr "" msgid "Open S&ubrepository" msgstr "" msgid "Open the selected subrepository" msgstr "" msgid "E&xplore Folder" msgstr "" msgid "Open the selected folder in the system file manager" msgstr "" msgid "Open &Terminal" msgstr "" msgid "Open a shell terminal in the selected folder" msgstr "" msgid "File(s) not found" msgstr "" msgid "The selected files do not exist in the working directory" msgstr "" msgid "Cannot display visual diff" msgstr "" msgid "Visual diffs are not supported for unapplied patches" msgstr "" msgid "Cannot open subrepository" msgstr "" msgid "The selected subrepository does not exist on the working directory" msgstr "" msgid "Display the file anyway" msgstr "" msgid "File or diffs not displayed: " msgstr "" #, python-format msgid "" "File is larger than the specified max size.\n" "maxdiff = %s KB" msgstr "" msgid "File is binary" msgstr "" msgid "File may be binary (maximum line length exceeded)" msgstr "" #, python-format msgid " (copied from %s)" msgstr "" #, python-format msgid " (renamed from %s)" msgstr "" msgid " (was added)" msgstr "" msgid "exec mode has been set" msgstr "" msgid "exec mode has been unset" msgstr "" msgid " (is a symlink)" msgstr "" msgid "Diff not displayed: " msgstr "" #, python-format msgid "changeset: %s" msgstr "" msgid "Initial revision" msgstr "" #, python-format msgid "" "[WARNING] Invalid subrepo revision ID:\n" "\t%s\n" "\n" msgstr "" msgid "Subrepo created and set to initial revision." msgstr "" msgid "Subrepo initialized to revision:" msgstr "" msgid "Subrepo removed from repository." msgstr "" msgid "Previously the subrepository was at the following revision:" msgstr "" msgid "Subrepo was not changed." msgstr "" msgid "[WARNING] Missing subrepo. Update to this revision to clone it." msgstr "" msgid "[WARNING] Incomplete subrepo. Update to this revision to pull it." msgstr "" msgid "Subrepo state is:" msgstr "" msgid "Revision has changed to:" msgstr "" #, python-format msgid "changeset: %s (not found on subrepository)" msgstr "" msgid "From:" msgstr "" msgid "" "[WARNING] Missing changed subrepository. Update to this revision to clone it." msgstr "" msgid "Subrepository not found in the working directory." msgstr "" msgid "" "[WARNING] Incomplete changed subrepository. Update to this revision to pull " "it." msgstr "" msgid "Not a Mercurial subrepo, not previewable" msgstr "" #, python-format msgid "Error previewing subrepo: %s" msgstr "" msgid "Subrepo may be damaged or inaccessible." msgstr "" msgid "The subrepository is dirty." msgstr "" msgid "File Status:" msgstr "" msgid "(is a changed sub-repository)" msgstr "" msgid "(is an unchanged sub-repository)" msgstr "" msgid "(is a dirty sub-repository)" msgstr "" msgid "(is a new sub-repository)" msgstr "" msgid "(is a removed sub-repository)" msgstr "" msgid "(is a changed and dirty sub-repository)" msgstr "" msgid "(is a new and dirty sub-repository)" msgstr "" msgid "open..." msgstr "" #, python-format msgid "" "File or diffs not displayed: File is larger than the specified max size.\n" "maxdiff = %s KB" msgstr "" msgid " (was deleted)" msgstr "" msgid " (was added, now missing)" msgstr "" msgid " (is unversioned)" msgstr "" #, python-format msgid "Hg file log viewer [%s] - %s" msgstr "" msgid "File History Log Columns" msgstr "" msgid "Back" msgstr "" msgid "Forward" msgstr "" msgid "&Diff Selected Changesets" msgstr "" msgid "Diff Selected &File Revisions" msgstr "" msgid "&Diff Changeset to Parent" msgstr "" msgid "Diff Changeset to &Local" msgstr "" msgid "Diff &File to Parent" msgstr "" msgid "Diff File to Lo&cal" msgstr "" msgid "Show Revision &Details" msgstr "" msgid "You must select two revisions to diff" msgstr "" msgid "Too many rows selected for menu" msgstr "" msgid "File Differences Log Columns" msgstr "" msgid "Next diff" msgstr "" msgid "Previous diff" msgstr "" msgctxt "column header" msgid "Filename" msgstr "" msgid " (excluded from the next commit)" msgstr "" msgid "View change as unified diff output" msgstr "" msgid "View change in context of file" msgstr "" msgid "annotate with revision numbers" msgstr "" msgid "Next diff (alt+down)" msgstr "" msgid "Previous diff (alt+up)" msgstr "" msgid "Show changes from first parent" msgstr "" msgid "Show changes from second parent" msgstr "" msgid "Open shelve tool" msgstr "" msgid "Mark excluded changes" msgstr "" msgid "&Search in Current File" msgstr "" msgid "Search in All &History" msgstr "" msgid "Annotate Op&tions" msgstr "" msgid "Search Selected Text" msgstr "" msgid "In Current &File" msgstr "" msgid "In &Current Revision" msgstr "" msgid "In &Original Revision" msgstr "" msgid "In All &History" msgstr "" msgid "Go to" msgstr "" msgid "View File at" msgstr "" msgid "Diff File to" msgstr "" msgid "&Originating Revision" msgstr "" msgid "&Local" msgstr "" #, python-format msgid "&Parent Revision (%d)" msgstr "" msgid "Show &Author" msgstr "" msgid "Show &Date" msgstr "" msgid "Show &Revision" msgstr "" msgid "Interrupted graft operation found" msgstr "" msgid "" "An interrupted graft operation has been found.\n" "\n" "You cannot perform a different graft operation unless you abort the " "interrupted graft operation first." msgstr "" msgid "Continue or abort interrupted graft operation?" msgstr "" #, python-format msgid "Graft %d changesets on top of changeset %s" msgstr "" msgid "To graft destination" msgstr "" msgid "Use my user name instead of graft committer user name" msgstr "" msgid "Use current date" msgstr "" msgid "Append graft info to log message" msgstr "" msgid "Graft" msgstr "" msgid "Abort" msgstr "" #, python-format msgid "Graft - %s" msgstr "" msgid "Graft changeset" msgstr "" #, python-format msgid "Graft changeset #%d of %d" msgstr "" msgid "" "Before graft, you must commit or discard changes." msgstr "" msgid "You may continue or start the graft" msgstr "" msgid "Graft is complete" msgstr "" msgid "Graft aborted" msgstr "" msgid "Graft failed" msgstr "" msgid "" "Graft generated merge conflicts that must be resolved" msgstr "" msgid "You may continue the graft" msgstr "" msgid "Exiting with an unfinished graft is not recommended." msgstr "" msgid "Consider aborting the graft first." msgstr "" msgid "&Exit" msgstr "" msgid "### regular expression search pattern ###" msgstr "" msgid "Regexp:" msgstr "" msgid "Ignore case" msgstr "" msgid "Search" msgstr "" msgid "Working Copy" msgstr "" msgid "All History" msgstr "" msgid "Report only the first match per file" msgstr "" msgid "Follow copies and renames" msgstr "" msgid "Includes:" msgstr "" msgid "Excludes:" msgstr "" msgid "" "Comma separated list of exclusion file patterns. Exclusion patterns are " "applied after inclusion patterns." msgstr "" msgid "" "Comma separated list of inclusion file patterns. By default, the entire " "repository is searched." msgstr "" msgid "TortoiseHg Search" msgstr "" #, python-format msgid "\"%s\" removed from search history" msgstr "" #, python-format msgid "\"%s\" removed from path history" msgstr "" #, python-format msgid "grep: invalid match pattern: %s\n" msgstr "" #, python-format msgid "grep: %s\n" msgstr "" #, python-format msgid "%d matches found" msgstr "" msgid "No matches found" msgstr "" msgid "Searching" msgstr "" msgid "history" msgstr "" msgid "Interrupted" msgstr "" msgid "files" msgstr "" #, python-format msgid "Skipping %s, unable to read" msgstr "" msgid "Vi&ew File" msgstr "" msgid "&View Changeset" msgstr "" msgid "Annotate &File" msgstr "" msgid "File" msgstr "" msgid "Line" msgstr "" msgid "Rev" msgstr "" msgid "User" msgstr "" msgid "Match Text" msgstr "" #, python-format msgid "Detect Copies/Renames in %s" msgstr "" msgid "Unrevisioned Files" msgstr "" msgid "Refresh file list" msgstr "" #, python-format msgid "Min Similarity: %d%%" msgstr "" msgid "Only consider deleted files" msgstr "" msgid "Uncheck to consider all revisioned files for copy sources" msgstr "" msgid "Find Renames" msgstr "" msgid "Find copy and/or rename sources" msgstr "" msgid "Candidate Matches" msgstr "" msgid "Accept All Matches" msgstr "" msgid "Accept Selected Matches" msgstr "" msgid "Differences from Source to Dest" msgstr "" msgid "Search already in progress" msgstr "" msgid "Cannot start a new search" msgstr "" msgid "No files to find" msgstr "" msgid "There are no files that may have been renamed" msgstr "" msgid "Multiple sources chosen" msgstr "" #, python-format msgid "" "You have multiple renames selected for destination file:\n" "%s. Aborting!" msgstr "" #, python-format msgid "" "%s and %s have identical contents\n" "\n" msgstr "" #. i18n: percent format #, python-format msgid "%d%%" msgstr "" msgid "Source" msgstr "" msgid "Dest" msgstr "" msgid "% Match" msgstr "" msgid "Sending Email" msgstr "" msgid "Email" msgstr "" msgid "To:" msgstr "" msgid "Cc:" msgstr "" msgid "In-Reply-To:" msgstr "" msgid "Message identifier to reply to, for threading" msgstr "" msgid "Flag:" msgstr "" msgid "" "Hg patches (as generated by export command) are compatible with most patch " "programs. They include a header which contains the most important changeset " "metadata." msgstr "" msgid "Send changesets as Hg patches" msgstr "" msgid "" "Git patches can describe binary files, copies, and permission changes, but " "recipients may not be able to use them if they are not using git or " "Mercurial." msgstr "" msgid "Use extended (git) patch format" msgstr "" msgid "" "Stripping Mercurial header removes username and parent information. Only " "useful if recipient is not using Mercurial (and does not like to see the " "headers)." msgstr "" msgid "Plain, do not prepend Hg header" msgstr "" msgid "" "Bundles store complete changesets in binary form. Upstream users can pull " "from them. This is the safest way to send changes to recipient Mercurial " "users." msgstr "" msgid "Send single binary bundle, not patches" msgstr "" msgid "send patches as part of the email body" msgstr "" msgid "body" msgstr "" msgid "send patches as attachments" msgstr "" msgid "attach" msgstr "" msgid "send patches as inline attachments" msgstr "" msgid "inline" msgstr "" msgid "add diffstat output to messages" msgstr "" msgid "diffstat" msgstr "" msgid "" "Patch series description is sent in initial summary email with [PATCH 0 of " "N] subject. It should describe the effects of the entire patch series. " "When emailing a bundle, these fields make up the message subject and body. " "Flags is a comma separated list of tags which are inserted into the message " "subject prefix." msgstr "" msgid "Write patch series (bundle) description" msgstr "" msgid "Subject:" msgstr "" msgid "Changesets" msgstr "" msgid "Select &All" msgstr "" msgid "Select &None" msgstr "" msgid "Edit" msgstr "" msgid "Preview" msgstr "" msgid "&Settings" msgstr "" msgid "Send &Email" msgstr "" #, python-format msgid "Ignore filter - %s" msgstr "" msgid "Glob" msgstr "" msgid "Regexp" msgstr "" msgid "Add" msgstr "Ekle" msgid "Edit File" msgstr "" msgid "Ignore Filter" msgstr "" msgid "Untracked Files" msgstr "" msgid "Backspace or Del to remove row(s)" msgstr "" msgid "Add ignore filter..." msgstr "" msgid "selected files" msgstr "" msgid "Ignore " msgstr "" msgid "Invalid glob expression" msgstr "" msgid "Invalid regexp expression" msgstr "" msgid "Unable to read repository status" msgstr "" msgid "New file created" msgstr "" msgid "" "TortoiseHg has created a new .hgignore file. Would you like to add this " "file to the source code control repository?" msgstr "" msgid "Unable to write .hgignore file" msgstr "" msgid "Add special files (.hgignore, ...)" msgstr "" msgid "Make repo compatible with Mercurial <1.7" msgstr "" msgid "Show in Workbench after init" msgstr "" msgid "Create" msgstr "" msgid "New Repository" msgstr "" msgid "Error executing init" msgstr "" msgid "Destination path is empty" msgstr "" msgid "Please enter the directory path" msgstr "" msgid "Init" msgstr "" #, python-format msgid "Are you sure about adding the new repository %d extra levels deep?" msgstr "" #, python-format msgid "" "Path exists up to:\n" "%s\n" "and you asked for:\n" "%s" msgstr "" #, python-format msgid "Cannot create folder %s" msgstr "" msgid "Unable to create new repository" msgstr "" msgid "Error when creating repository" msgstr "" #, python-format msgid "

    Repository successfully created at

    %s

    " msgstr "" msgid "Unable to create a config file" msgstr "" msgid "Insufficient access rights." msgstr "" #, python-format msgid "?? Error: %s ??" msgstr "" msgid "" "Some of the files that you have selected are of a size over 10 MB. You may " "make more efficient use of disk space by adding these files as largefiles, " "which will store only the most recent revision of each file in your local " "repository, with older revisions available on the server. Do you wish to " "add these files as largefiles?" msgstr "" msgid "Add as &Largefiles" msgstr "" msgid "Add as &Normal Files" msgstr "" msgid "License" msgstr "" msgid "Drag to change order" msgstr "" msgid "Workbench" msgstr "" #, python-format msgid "Manifest %s@%s" msgstr "" msgid "### filter text ###" msgstr "" msgid "Filter:" msgstr "" msgid "Status" msgstr "" msgid "Find revisions matching fields of:" msgstr "" msgid "Revision to Match:" msgstr "" msgid "Fields to match:" msgstr "" msgid "Summary (first description line)" msgstr "" msgid "Description" msgstr "" msgid "Author" msgstr "" msgid "Date" msgstr "" msgid "Files" msgstr "" msgid "Diff contents" msgstr "" msgid "Subrepo states" msgstr "" msgid "Branch" msgstr "" msgid "Phase" msgstr "" msgid "&Match" msgstr "" #, python-format msgid "Find matches - %s" msgstr "" msgid "Revisions to Match:" msgstr "" #, python-format msgid "Match any of %d revisions" msgstr "" msgid "Unknown revision!" msgstr "" msgid "Parse Error!" msgstr "" #, python-format msgid "Merge - %s" msgstr "" msgid "Do you want to exit?" msgstr "" msgid "" "To finish merging, you must commit the working directory.\n" "\n" "To cancel the merge you can update to one of the merge parent revisions." msgstr "" msgid "Prepare to merge" msgstr "" msgid "Verify merge targets and ensure your working directory is clean." msgstr "" msgid "Not a head revision!" msgstr "" msgid "Merge from (other revision)" msgstr "" msgid "Unable to merge" msgstr "" msgid "Merge revision not specified or not found" msgstr "" msgid "Merge to (working directory)" msgstr "" msgid "" "The working directory is already merged. Continue or discard existing " "merge." msgstr "" msgid "" "Before merging, you must commit, shelve to patch, or discard changes." msgstr "" msgid "Or use:" msgstr "" msgid "Force a merge with outstanding changes (-f/--force)" msgstr "" msgid "Discard all changes from merge target (other) revision" msgstr "" msgid "&Discard" msgstr "" msgid "Confirm Discard Changes" msgstr "" #, python-format msgid "" "The changes from revision %s and all unmerged parents will be discarded.\n" "\n" "Are you sure this is what you want to do?" msgstr "" msgctxt "working dir state" msgid "Clean" msgstr "" msgid "Merging..." msgstr "" msgid "Automatically advance to next page when merge is complete." msgstr "" #, python-format msgid "" "%d files were modified on both branches and must be resolved" msgstr "" msgid "Commit merge results" msgstr "" msgid "Commit Options" msgstr "" msgid "Commit Now" msgstr "" msgid "Commit Later" msgstr "" msgid "Merge" msgstr "" #, python-format msgid "Merge with %s" msgstr "" msgid "TortoiseHg Merge Commit" msgstr "" #, python-format msgid "" "Error creating interpreting commit date (%s).\n" "Using current date instead." msgstr "" msgid "Merge changeset" msgstr "" msgid "Syntax Highlighting" msgstr "" msgid "Paste &Filenames" msgstr "" msgid "App&ly Format" msgstr "" msgid "C&onfigure Format" msgstr "" msgid "&Commit to Queue..." msgstr "" msgid "Create &New Queue..." msgstr "" msgid "&Rename Active Queue..." msgstr "" msgid "&Delete Queue..." msgstr "" msgid "&Purge Queue..." msgstr "" msgid "Create Patch Queue" msgstr "" msgid "New patch queue name" msgstr "" msgid "Rename Patch Queue" msgstr "" #, python-format msgid "Rename patch queue '%s' to" msgstr "" msgid "Rename" msgstr "Yeniden Adlandır" msgid "Delete Patch Queue" msgstr "" msgid "Delete reference to" msgstr "" msgid "Delete" msgstr "" msgid "Purge Patch Queue" msgstr "" msgid "Remove patch directory of" msgstr "" msgid "Purge" msgstr "" msgid "Rename Patch" msgstr "" #, python-format msgid "Rename patch %s to:" msgstr "" msgid "no guards" msgstr "" msgid "Patch Queue" msgstr "Paket Kuyruğu" msgctxt "MQ QPush" msgid "Push all" msgstr "" msgid "Apply all patches" msgstr "" msgctxt "MQ QPush" msgid "Push" msgstr "" msgid "Apply one patch" msgstr "" msgid "Set &Guards..." msgstr "" msgid "Configure guards for selected patch" msgstr "" msgid "&Delete Patches..." msgstr "" msgid "Delete selected patches" msgstr "" msgid "Pop" msgstr "" msgid "Unapply one patch" msgstr "" msgid "Pop all" msgstr "" msgid "Unapply all patches" msgstr "" msgid "Re&name Patch..." msgstr "" msgid "Patch Queue Actions Toolbar" msgstr "" msgid "Configure guards" msgstr "" #, python-format msgid "Input new guards for %s:" msgstr "" msgid "Confirm patch queue switch" msgstr "" #, python-format msgid "Do you really want to activate patch queue '%s' ?" msgstr "" #, python-format msgid "Guards: %d/%d" msgstr "" msgid "MQ options" msgstr "" msgid "Force push or pop (--force)" msgstr "" msgid "Tolerate non-conflicting local changes (--keep-changes)" msgstr "" msgid "### patch name ###" msgstr "" #, python-format msgid "%s had rejected chunks, edit patched file together with rejects?" msgstr "" msgid "Patch Name Required" msgstr "" msgid "You must enter a patch name" msgstr "" #, python-format msgid "Pending Perforce Changelists - %s" msgstr "" msgid "Submitting p4 changelist..." msgstr "" msgid "Reverting p4 changelist..." msgstr "" msgid "Edit Repository URL" msgstr "" msgid "Patch Branch Toolbar" msgstr "" msgid "Merge all pending dependencies" msgstr "" msgid "Backout current patch branch" msgstr "" msgid "Backport part of a changeset to a dependency" msgstr "" msgid "Start a new patch branch" msgstr "" msgid "Edit patch dependency graph" msgstr "" msgid "will be closed" msgstr "" #, python-format msgid "needs merge of %i heads\n" msgstr "" #, python-format msgid "needs merge with %s (through %s)\n" msgstr "" #, python-format msgid "needs merge with %s\n" msgstr "" #, python-format msgid "needs update of diff base to tip of %s\n" msgstr "" msgid "&Goto (update workdir)" msgstr "" msgid "&Merge" msgstr "" msgid "No patch branch selected" msgstr "" msgid "No editor found" msgstr "" msgid "" "Mercurial was unable to find an editor. Please configure Mercurial to use an " "editor installed on your system." msgstr "" msgid "Graph" msgstr "" msgid "Title" msgstr "" msgid "Message" msgstr "" msgid "New Patch Branch" msgstr "" msgid "Patch message:" msgstr "" msgid "Patch date:" msgstr "" msgid "Patch user:" msgstr "" msgid "Invalid Settings - The ReviewBoard server is not setup" msgstr "" msgid "Invalid Settings - Please provide your ReviewBoard username" msgstr "" #, python-format msgid "" "Invalid reviewboard plugin. Please download the Mercurial reviewboard plugin " "version 3.5 or higher from the website below.\n" "\n" " %s" msgstr "" msgid "Review Board" msgstr "" msgid "Password:" msgstr "" msgid "Error" msgstr "" #, python-format msgid "Review draft posted to %s\n" msgstr "" #, python-format msgid "Review published to %s\n" msgstr "" msgid "Success" msgstr "" msgid "Repository ID:" msgstr "" msgid "Post Review" msgstr "" msgid "Review ID:" msgstr "" msgid "Update the fields of this existing request" msgstr "" msgid "Update Review" msgstr "" msgid "Create diff with all outgoing changes" msgstr "" msgid "Create diff with all changes on this branch" msgstr "" msgid "Publish request immediately" msgstr "" msgid "%p%" msgstr "" msgid "Connecting to Review Board..." msgstr "" msgid "Post &Review" msgstr "" msgid "No unknown files found" msgstr "" msgid "No ignored files found" msgstr "" msgid "No trash files found" msgstr "" msgid "Delete empty folders" msgstr "" msgid "Preserve files beginning with .hg" msgstr "" #, python-format msgid "%s - purge" msgstr "" msgid "Checking" msgstr "" msgid "Ready to purge." msgstr "" #, python-format msgid "Delete %d unknown file" msgid_plural "Delete %d unknown files" msgstr[0] "" msgstr[1] "" #, python-format msgid "Delete %d ignored file" msgid_plural "Delete %d ignored files" msgstr[0] "" msgstr[1] "" #, python-format msgid "Delete %d file in .hg/Trashcan" msgid_plural "Delete %d files in .hg/Trashcan" msgstr[0] "" msgstr[1] "" msgid "Confirm file deletions" msgstr "" msgid "Are you sure you want to delete these files and/or folders?" msgstr "" msgid "Deletion failures" msgstr "" #, python-format msgid "Unable to delete %d file or folder" msgid_plural "Unable to delete %d files or folders" msgstr[0] "" msgstr[1] "" msgid "Deleting trash folder..." msgstr "" #, python-format msgid "Deleted %d files" msgstr "" #, python-format msgid "Deleted %d files and %d folders" msgstr "" msgid "Delete Patches" msgstr "" msgid "Remove patches from queue?" msgstr "" msgid "Keep patch files" msgstr "" #, python-format msgid "Patch fold - %s" msgstr "" msgid "New patch message:" msgstr "" msgid "Patches to fold" msgstr "" msgid "Rename Error" msgstr "" msgid "Could not rename existing patchfile" msgstr "" msgid "Could not delete existing patchfile" msgstr "" msgid "QRename - Check patchname" msgstr "" #, python-format msgid "Patch name %s already exists:" msgstr "" msgid "Add .OLD extension to existing patchfile" msgstr "" msgid "Overwrite existing patchfile" msgstr "" msgid "Go back and change new patchname" msgstr "" msgid "&Undo" msgstr "" msgid "&Redo" msgstr "" msgid "Cu&t" msgstr "" msgid "&Copy" msgstr "" msgid "&Paste" msgstr "" msgid "&Delete" msgstr "" msgid "&Editor Options" msgstr "" msgid "&Wrap" msgstr "" msgctxt "wrap mode" msgid "&None" msgstr "" msgid "&Word" msgstr "" msgid "&Character" msgstr "" msgid "White&space" msgstr "" msgid "&Visible" msgstr "" msgid "&Invisible" msgstr "" msgid "&AfterIndent" msgstr "" msgid "&TAB Inserts" msgstr "" msgid "&Auto" msgstr "" msgid "&TAB" msgstr "" msgid "&Spaces" msgstr "" msgid "EOL &Visibility" msgstr "" msgid "EOL &Mode" msgstr "" msgid "&Windows" msgstr "" msgid "&Unix" msgstr "" msgid "&Mac" msgstr "" msgid "&Auto-Complete" msgstr "" msgid "### regular expression ###" msgstr "" msgid "Regular expression search pattern" msgstr "" msgid "Wrap search" msgstr "" msgid "Prev" msgstr "" msgid "Next" msgstr "" msgid "Unable to read/write config file" msgstr "" msgid "Try refreshing your repository." msgstr "" #, python-format msgid "" "Error string \"%(arg0)s\" at %(arg1)s
    Please edit your config" msgstr "" #, python-format msgid "" "Configuration Error: \"%(arg0)s\",
    Please fix your config" msgstr "" #, python-format msgid "Operation aborted:

    %(arg0)s." msgstr "" msgid "Repository is locked" msgstr "" msgid "hint:" msgstr "" msgid "Keyboard Interrupt" msgstr "" msgid "Close this application?" msgstr "" msgid "Repository Error" msgstr "" msgid "No visual editor configured" msgstr "" msgid "Please configure a visual editor." msgstr "" msgid "Editor launch failure" msgstr "" msgid "Save file to" msgstr "" msgid "Unable to save file" msgstr "Dosya kaydedilemedi" msgid "Failed to open path in terminal" msgstr "" #, python-format msgid "\"%s\" is not a valid directory" msgstr "" msgid "Unable to start the following command:" msgstr "" msgid "No shell configured" msgstr "" msgid "A terminal shell must be configured" msgstr "" msgid "Show Log" msgstr "" msgid "Please enter a username" msgstr "" msgid "You must identify yourself to Mercurial" msgstr "" msgid "Text Translation Failure" msgstr "" msgid "Unable to translate input to local encoding." msgstr "" msgid "Checkmark files to add" msgstr "" msgid "Checkmark files to forget" msgstr "" msgid "Forget" msgstr "" msgid "Checkmark files to revert" msgstr "" msgid "Revert" msgstr "" msgid "Checkmark files to remove" msgstr "" msgid "Remove" msgstr "Kaldır" #, python-format msgid "%s - hg %s" msgstr "" msgid "Do not save backup files (*.orig)" msgstr "" msgid "Force removal of modified files (--force)" msgstr "" msgid "Add &Largefiles" msgstr "" msgid "No files selected" msgstr "" msgid "No operation to perform" msgstr "" msgid "" "You have selected one or more files that have been modified. By default, " "these files will not be removed. What would you like to do?" msgstr "" msgid "Remove &Unmodified Files" msgstr "" msgid "Remove &All Selected Files" msgstr "" msgid "Rebase changeset and descendants" msgstr "" msgid "To rebase destination" msgstr "" msgid "Swap source and destination" msgstr "" msgid "Keep original changesets" msgstr "" msgid "Keep original branch names" msgstr "" msgid "Collapse the rebased changesets " msgstr "" msgid "Rebase entire source branch" msgstr "" msgid "Rebase unpublished onto Subversion head (override source, destination)" msgstr "" msgid "Rebase" msgstr "" #, python-format msgid "Rebase - %s" msgstr "" msgid "" "Before rebase, you must
    commit or discard changes." msgstr "" msgid "You may continue the rebase" msgstr "" msgid "Rebase is complete" msgstr "" msgid "Rebase aborted" msgstr "" msgid "Rebase failed" msgstr "" msgid "" "Rebase generated merge conflicts that must be resolved" msgstr "" msgid "Exiting with an unfinished rebase is not recommended." msgstr "" msgid "Consider aborting the rebase first." msgstr "" #, python-format msgid "Merge rejected patch chunks into %s" msgstr "" msgid "Mark this chunk as resolved, goto next unresolved" msgstr "" msgid "Mark this chunk as unresolved" msgstr "" msgid "Unable to merge rejects" msgstr "" msgid "Can't read this file (maybe deleted)" msgstr "" msgid "This appears to be a binary file" msgstr "" msgid "Warning" msgstr "" msgid "" "You have marked all rejected patch chunks as resolved yet you have not " "modified the file on the edit panel.\n" "\n" "This probably means that no code from any of the rejected patch chunks made " "it into the file.\n" "\n" "Are you sure that you want to leave the file as is and consider all the " "rejected patch chunks as resolved?\n" "\n" "Doing so may delete them from a shelve, for example, which would mean that " "you would lose them forever!\n" "\n" "Click Yes to accept the file as is or No to continue resolving the rejected " "patch chunks." msgstr "" msgid "Copy source -> destination" msgstr "" #, python-format msgid "Copy - %s" msgstr "" msgid "Copy Error" msgstr "" #, python-format msgid "Rename - %s" msgstr "" msgid "Select Source File" msgstr "" msgid "Select Source Folder" msgstr "Kaynak dosyayı seç" msgid "Source does not exists." msgstr "" msgid "The source must be within the repository tree." msgstr "" msgid "The destination must be within the repository tree." msgstr "" msgid "Please give a destination that differs from the source" msgstr "" msgid "Destination file already exists." msgstr "" msgid "Are you sure you want to overwrite it ?" msgstr "" msgid "Cannot do a pure casefolding copy on Windows" msgstr "" msgid "Show all" msgstr "" msgid "### revision set query ###" msgstr "" msgid "Clear current query and query text" msgstr "" msgid "Trigger revision set query" msgstr "" msgid "Open advanced query editor" msgstr "" msgid "Delete selected query from history" msgstr "" msgid "filter" msgstr "" msgid "Toggle filtering of non-matched changesets" msgstr "" msgid "Show/Hide hidden changesets" msgstr "" msgid "Toggle graft relations visibility" msgstr "" msgid "Keyword Search" msgstr "" msgid "Revision Set" msgstr "" msgid "Display graph the named branch only" msgstr "" msgid "Display only active branches" msgstr "" msgid "Display closed branches" msgstr "" msgid "Include all ancestors" msgstr "" msgctxt "column header" msgid "Graph" msgstr "" msgctxt "column header" msgid "Rev" msgstr "" msgctxt "column header" msgid "Branch" msgstr "" msgctxt "column header" msgid "Description" msgstr "" msgctxt "column header" msgid "Author" msgstr "" msgctxt "column header" msgid "Tags" msgstr "" msgctxt "column header" msgid "Latest tags" msgstr "" msgctxt "column header" msgid "Node" msgstr "" msgctxt "column header" msgid "Age" msgstr "" msgctxt "column header" msgid "Local Time" msgstr "" msgctxt "column header" msgid "UTC Time" msgstr "" msgctxt "column header" msgid "Changes" msgstr "" msgctxt "column header" msgid "Converted From" msgstr "" msgctxt "column header" msgid "Phase" msgstr "" #, python-format msgid "filling (%d)" msgstr "" msgid "Mercurial User" msgstr "" #, python-format msgid "Unsupported repository type (%s)" msgstr "" msgid "Cannot open non Mercurial repositories or subrepositories" msgstr "" msgid "Confirm Delete" msgstr "" #, python-format msgid "Delete Group '%s' and all its entries?" msgstr "" msgid "Repository Registry" msgstr "" msgid "Show &Paths" msgstr "" msgid "Show S&hort Paths" msgstr "" msgid "&Scan Repositories at Startup" msgstr "" msgid "Scan &Remote Repositories" msgstr "" msgid "&Refresh Repository List" msgstr "" msgid "Refresh the Repository Registry list" msgstr "" msgid "&Open" msgstr "" msgid "Open the repository in a new tab" msgstr "" msgid "&Open All" msgstr "" msgid "Open all repositories in new tabs" msgstr "" msgid "New &Group" msgstr "" msgid "Create a new group" msgstr "" msgid "Rename the entry" msgstr "" msgid "Settin&gs" msgstr "" msgid "View the repository's settings" msgstr "" msgid "Re&move from Registry" msgstr "" msgid "" "Remove the node and all its subnodes. Repositories are not deleted from disk." msgstr "" msgid "Clon&e..." msgstr "" msgid "Clone Repository" msgstr "" msgid "E&xplore" msgstr "" msgid "Open the repository in a file browser" msgstr "" msgid "&Terminal" msgstr "" msgid "Open a shell terminal in the repository root" msgstr "" msgid "&Add Repository..." msgstr "" msgid "Add a repository to this group" msgstr "" msgid "A&dd Subrepository..." msgstr "" msgid "Convert an existing repository into a subrepository" msgstr "" msgid "Remo&ve Subrepository..." msgstr "" msgid "Remove this subrepository from the current revision" msgstr "" msgid "Copy the root path of the repository to the clipboard" msgstr "" msgid "Sort by &Name" msgstr "" msgid "Sort the group by short name" msgstr "" msgid "Sort by &Path" msgstr "" msgid "Sort the group by full path" msgstr "" msgid "&Sort by .hgsub" msgstr "" msgid "Order the subrepos as in .hgsub" msgstr "" msgid "Select repository directory to add" msgstr "" msgid "Select an existing repository to add as a subrepo" msgstr "" msgid "Cannot add subrepository" msgstr "" #, python-format msgid "%s is not a valid repository" msgstr "" #, python-format msgid "\"%s\" is not a folder" msgstr "" msgid "A repository cannot be added as a subrepo of itself" msgstr "" #, python-format msgid "" "The selected folder:

    %s

    is not inside the target repository." "

    This may be allowed but is greatly discouraged.
    If you want to " "add a non trivial subrepository mapping you must manually edit the ." "hgsub file" msgstr "" msgid "Cannot open repository" msgstr "" #, python-format msgid "The selected repository:

    %s

    cannot be open!" msgstr "" msgid "Subrepository already exists" msgstr "" #, python-format msgid "" "The selected repository:

    %s

    is already a subrepository of:" "

    %s

    as: \"%s\"" msgstr "" msgid "Failed to add subrepository" msgstr "" #, python-format msgid "Cannot open the .hgsub file in:

    %s" msgstr "" msgid "Failed to add repository" msgstr "" #, python-format msgid "The .hgsub file already contains the line:

    %s" msgstr "" msgid "Subrepo added to .hgsub file" msgstr "" #, python-format msgid "" "The selected subrepo:

    %s

    has been added to the .hgsub " "file of the repository:

    %s

    Remember that in order to " "finish adding the subrepo you must still commit the changes to " "the .hgsub file in order to confirm the addition of the subrepo." msgstr "" #, python-format msgid "Cannot update the .hgsub file in:

    %s" msgstr "" msgid "Could not open .hgsub file" msgstr "" msgid "Cannot read the .hgsub file.

    Subrepository removal failed." msgstr "" msgid "Subrepository not found" msgstr "" msgid "" "The selected subrepository was not found on the .hgsub file.

    Perhaps it " "has already been removed?" msgstr "" msgid "&Yes" msgstr "&Evet" msgid "&No" msgstr "&Hayır" msgid "Remove the selected repository?" msgstr "" #, python-format msgid "" "Do you really want to remove the repository \"%s\" from its parent " "repository \"%s\"" msgstr "" msgid "Subrepository removed from .hgsub" msgstr "" msgid "" "The selected subrepository has been removed from the .hgsub file.

    Remember " "that you must commit this .hgsub change in order to complete the removal of " "the subrepository!" msgstr "" msgid "Could not update .hgsub file" msgstr "" msgid "Cannot update the .hgsub file.

    Subrepository removal failed." msgstr "" msgid "New Group" msgstr "" msgid "Could not get subrepository list" msgstr "" #, python-format msgid "" "It was not possible to get the subrepository list for the repository in:" "

    %s" msgstr "" msgid "Could not open some subrepositories" msgstr "" #, python-format msgid "" "It was not possible to fully load the subrepository list for the repository " "in:

    %s

    The following subrepositories may be missing, " "broken or on an inconsistent state and cannot be accessed:

    %s" msgstr "" msgid "Updating repository registry" msgstr "" #, python-format msgid "Loading repository %s" msgstr "" msgid "Repository Registry updated" msgstr "" msgid "&Sort" msgstr "" #, python-format msgid "Local Repository %s" msgstr "" #, python-format msgid "" "An exception happened while loading the subrepos of:

    \"%s\"

    " msgstr "" #, python-format msgid "The exception error message was:

    %s

    " msgstr "" msgid "Click OK to continue or Abort to exit." msgstr "" msgid "Error loading subrepos" msgstr "" msgid "Unable to update repository name" msgstr "" #, python-format msgid "An error occurred while updating the repository hgrc file (%s)" msgstr "" msgid "default" msgstr "" msgid "Path" msgstr "" msgid "C&hoose Log Columns..." msgstr "" #, python-format msgid "Goto ancestor of %s and %s" msgstr "" #, python-format msgid "Can't find revision '%s'" msgstr "" msgid "Workbench Log Columns" msgstr "" msgctxt "tab tooltip" msgid "Revision details" msgstr "" msgctxt "tab tooltip" msgid "Commit" msgstr "" msgctxt "tab tooltip" msgid "Synchronize" msgstr "" msgctxt "tab tooltip" msgid "Manifest" msgstr "" msgctxt "tab tooltip" msgid "Search" msgstr "" msgctxt "tab tooltip" msgid "Patch Branch" msgstr "" #, python-format msgid "%s " msgstr "" #, python-format msgid "Found %d incoming changesets" msgstr "" msgid "Pull incoming changesets into your repository" msgstr "" msgid "Reject" msgstr "" msgid "Reject incoming changesets" msgstr "" #, python-format msgid "Push current branch (%s)" msgstr "" #, python-format msgid "Push up to current revision (#%d)" msgstr "" #, python-format msgid "Push up to revision #%d" msgstr "" msgid "Push all" msgstr "" msgid "no outgoing changesets" msgstr "" #, python-format msgid "no outgoing changesets in current branch (%s) / %d in total" msgstr "" #, python-format msgid "no outgoing changesets up to current revision (#%d) / %d in total" msgstr "" #, python-format msgid "no outgoing changesets up to revision #%d / %d in total" msgstr "" #, python-format msgid "%d outgoing changesets" msgstr "" #, python-format msgid "%d outgoing changesets in current branch (%s) / %d in total" msgstr "" #, python-format msgid "%d outgoing changesets up to current revision (#%d) / %d in total" msgstr "" #, python-format msgid "%d outgoing changesets up to revision #%d / %d in total" msgstr "" msgid "Nothing to push" msgstr "" #, python-format msgid "%s - verify repository" msgstr "" #, python-format msgid "%s - recover repository" msgstr "" msgid "No transaction available" msgstr "" msgid "There is no rollback transaction available" msgstr "" msgid "Undo last commit?" msgstr "Son işleme geri alınsın mı?" #, python-format msgid "Undo most recent commit (%d), preserving file changes?" msgstr "" msgid "Undo last transaction?" msgstr "" #, python-format msgid "Rollback to revision %d (undo %s)?" msgstr "" msgid "Unable to determine working copy revision\n" msgstr "" msgid "Remove current working revision?" msgstr "" #, python-format msgid "" "Your current working revision (%d) will be removed by this rollback, leaving " "uncommitted changes.\n" " Continue?" msgstr "" msgid "Repository stripped, incoming preview cleared" msgstr "" msgid "Repository stripped, revision set cleared" msgstr "" msgid "Commit tab cannot exit" msgstr "" msgid "Sync tab cannot exit" msgstr "" msgid "Search tab cannot exit" msgstr "" msgid "Repository command still running" msgstr "" msgid "Pus&h" msgstr "" msgid "Push to &Here" msgstr "" msgid "Push Selected &Branch" msgstr "" msgid "Push &All" msgstr "" msgid "&Update..." msgstr "" msgid "Bro&wse at Revision" msgstr "" msgid "&Similar Revisions..." msgstr "" msgid "&Merge with Local..." msgstr "" msgid "&Tag..." msgstr "" msgid "Boo&kmark..." msgstr "" msgid "Sig&n..." msgstr "" msgid "&Backout..." msgstr "" msgid "Copy &Hash" msgstr "" msgid "E&xport" msgstr "" msgid "E&xport Patch..." msgstr "" msgid "&Email Patch..." msgstr "" msgid "&Archive..." msgstr "" msgid "&Bundle Rev and Descendants..." msgstr "" msgid "&Copy Patch" msgstr "" msgid "Change &Phase to" msgstr "" msgid "&Graft to Local..." msgstr "" msgid "Modi&fy History" msgstr "" msgid "&Unapply Patch" msgstr "" msgid "Import to &MQ" msgstr "" msgid "&Finish Patch" msgstr "" msgid "MQ &Options" msgstr "" msgid "&Rebase..." msgstr "" msgid "&Strip..." msgstr "" msgid "Post to Re&view Board..." msgstr "" msgid "&Remote Update..." msgstr "" msgid "Write diff file" msgstr "" msgid "Unable to write diff file" msgstr "" msgid "Unable to compress history" msgstr "" msgid "Selected changeset pair not related" msgstr "" msgid "Visual Diff..." msgstr "" msgid "Export Diff..." msgstr "" msgid "Export Selected..." msgstr "" msgid "Email Selected..." msgstr "" msgid "Copy Selected as Patch" msgstr "" msgid "Export DAG Range..." msgstr "" msgid "Email DAG Range..." msgstr "" msgid "Bundle DAG Range..." msgstr "" msgid "Bisect - Good, Bad..." msgstr "" msgid "Bisect - Bad, Good..." msgstr "" msgid "Compress History..." msgstr "" msgid "Rebase..." msgstr "" msgid "Goto common ancestor" msgstr "" msgid "Similar revisions..." msgstr "" msgid "Graft Selected to local..." msgstr "" msgid "Post Selected to Review Board..." msgstr "" msgid "Apply patch" msgstr "" msgid "Apply onto original parent" msgstr "" msgid "Apply only this patch" msgstr "" msgid "Fold patches..." msgstr "" msgid "Delete patches..." msgstr "" msgid "Rename patch..." msgstr "" msgid "Pull to here..." msgstr "" msgid "Visual diff..." msgstr "" msgid "Export patch" msgstr "" msgid "Patch Files (*.patch)" msgstr "" msgid "Cannot export revision" msgstr "" #, python-format msgid "" "Cannot export revision %s into the file named:\n" "\n" "%s\n" msgstr "" msgid "There is already an existing folder with that same name." msgstr "" msgid "Replace" msgstr "" msgid "Append" msgstr "" #, python-format msgid "" "There are existing patch files for %d revisions (%s) in the selected " "location (%s).\n" "\n" msgstr "" msgid "What do you want to do?\n" msgstr "" msgid "Replace the existing patch files.\n" msgstr "" msgid "Append the changes to the existing patch files.\n" msgstr "" msgid "Abort the export operation.\n" msgstr "" msgid "Patch files already exist" msgstr "" msgid "Patch exported" msgstr "" #, python-format msgid "" "Revision #%d (%s) was exported to:

    %s%s%s" msgstr "" msgid "Patches exported" msgstr "" #, python-format msgid "%d patches were exported to:

    %s" msgstr "" msgid "You cannot merge a revision with itself" msgstr "" msgid "Write bundle" msgstr "" msgid "Backwards phase change requested" msgstr "" msgid "Do you really want to make this revision secret?" msgstr "" msgid "" "Making a \"draft\" revision \"secret\" is generally a safe " "operation.\n" "\n" "However, there are a few caveats:\n" "\n" "- \"secret\" revisions are not pushed. This can cause you trouble if you\n" "refer to a secret subrepo revision.\n" "\n" "- If you pulled this revision from a non publishing server it may be\n" "moved back to \"draft\" if you pull again from that particular " "server.\n" "\n" "Please be careful!" msgstr "" msgid "&Make secret" msgstr "" msgid "&Cancel" msgstr "&İptal" msgid "Do you really want to force a backwards phase transition?" msgstr "" #, python-format msgid "" "You are trying to move the phase of revision %d backwards,\n" "from \"%s\" to \"%s\".\n" "\n" "However, \"%s\" is a lower phase level than \"%s\".\n" "\n" "Moving the phase backwards is not recommended.\n" "For example, it may result in having multiple heads\n" "if you modify a revision that you have already pushed\n" "to a server.\n" "\n" "Please be careful!" msgstr "" msgid "&Force" msgstr "" msgid "Cannot import selected revision" msgstr "" #, python-format msgid "" "The selected revision (rev #%d) cannot be imported because it is not a " "descendant of qparent (rev #%d)" msgstr "" msgid "Invalid command" msgstr "" msgid "The selected command is empty" msgstr "" #, python-format msgid "" "The following error message was returned:\n" "\n" "%s" msgstr "" msgid "" "\n" "\n" "Please check that the \"thg\" command is valid." msgstr "" msgid "Failed to execute custom TortoiseHg command" msgstr "" #, python-format msgid "The command \"%s\" failed (code %d)." msgstr "" msgid "Failed to execute custom command" msgstr "" #, python-format msgid "The command \"%s\" could not be executed." msgstr "" #, python-format msgid "" "The following error message was returned:\n" "\n" "\"%s\"\n" "\n" "Please check that the command path is valid and that it is a valid " "application" msgstr "" #, python-format msgid "Resolve Conflicts - %s" msgstr "" msgid "Local revision information" msgstr "" msgid "Other revision information" msgstr "" msgid "Unresolved conflicts" msgstr "" msgid "Mercurial Re&solve" msgstr "" msgid "Attempt automatic (trivial) merge" msgstr "" msgid "Tool &Resolve" msgstr "" msgid "Merge using selected merge tool" msgstr "" msgid "&Take Local" msgstr "" msgid "Accept the local file version (yours)" msgstr "" msgid "Take &Other" msgstr "" msgid "Accept the other file version (theirs)" msgstr "" msgid "&Mark as Resolved" msgstr "" msgid "Mark this file as resolved" msgstr "" msgid "Diff &Local to Ancestor" msgstr "" msgid "&Diff Other to Ancestor" msgstr "" msgid "Resolved conflicts" msgstr "" msgid "&Edit File" msgstr "" msgid "Edit resolved file" msgstr "" msgid "3-&Way Diff" msgstr "" msgid "Visual three-way diff" msgstr "" msgid "Visual diff between resolved file and first parent" msgstr "" msgid "&Diff to Other" msgstr "" msgid "Visual diff between resolved file and second parent" msgstr "" msgid "Mark as &Unresolved" msgstr "" msgid "Mark this file as unresolved" msgstr "" msgid "Detected merge/diff tools:" msgstr "" msgid "Command output" msgstr "" msgid "Unable to show subrepository files" msgstr "" msgid "" "Visual diffs are not supported for files in subrepositories. They will not " "be shown." msgstr "" msgid "There are merge conflicts to be resolved" msgstr "" msgid "All conflicts are resolved." msgstr "" msgid "There are no conflicting file merges." msgstr "" msgid "Exit without finishing resolve?" msgstr "" msgid "Unresolved conflicts remain. Are you sure?" msgstr "" msgid "E&xit" msgstr "" msgid "Ext" msgstr "" msgid "Repository" msgstr "" msgid "" msgstr "" msgid "File List Toolbar" msgstr "" msgid "Update to this revision" msgstr "" msgid "Show All" msgstr "" msgid "Toggle display of all files and the direction they were merged" msgstr "" #, python-format msgid "%s - Revision Details (%s)" msgstr "" #, python-format msgid "Revert - %s" msgstr "" #, python-format msgid "Revert %s to its contents at the following revision?" msgstr "" #, python-format msgid "Revert %d files to their contents at the following revision?" msgstr "" msgid "Revert all files to this revision" msgstr "" #, python-format msgid "revision %d's first parent (i.e. revision %d)" msgstr "" #, python-format msgid "revision %d's second parent (i.e. revision %d)" msgstr "" msgid "null revision (i.e. remove file(s))" msgstr "" msgid "Confirm Revert" msgstr "" msgid "" "Reverting all files will discard changes and leave affected files in a " "modified state.

    Are you sure you want to use revert?

    (use " "update to checkout another revision)" msgstr "" msgid "Changeset:" msgstr "" msgid "Child:" msgstr "" msgid "Precursors:" msgstr "" msgid "Successors:" msgstr "" msgid "Head is closed!" msgstr "" msgid "Changesets where username contains string." msgstr "" msgid "" "Search commit message, user name, and names of changed files for string." msgstr "" msgid "Like \"keyword(string)\" but accepts a regex." msgstr "" msgid "" "Changesets not found in the specified destination repository, or the default " "push location." msgstr "" msgid "The named bookmark or all bookmarks." msgstr "" msgid "The named tag or all tags." msgstr "" msgid "Changeset is tagged." msgstr "" msgid "Changeset is a named branch head." msgstr "" msgid "Changeset is a merge changeset." msgstr "" msgid "Changeset is closed." msgstr "" msgid "" "Changesets within the interval, see help dates" msgstr "" msgid "Greatest common ancestor of the two changesets." msgstr "" msgid "" "Find revisions that \"match\" one or more fields of the given set of " "revisions." msgstr "" msgid "" "Changesets affecting files matched by pattern. See help patterns" msgstr "" msgid "Changesets which modify files matched by pattern." msgstr "" msgid "Changesets which add files matched by pattern." msgstr "" msgid "Changesets which remove files matched by pattern." msgstr "" msgid "Changesets containing files matched by pattern." msgstr "" msgid "All changesets belonging to the branches of changesets in set." msgstr "" msgid "Members of a set with no children in set." msgstr "" msgid "Changesets which are descendants of changesets in set." msgstr "" msgid "Changesets that are ancestors of a changeset in set." msgstr "" msgid "Child changesets of changesets in set." msgstr "" msgid "The set of all parents for all changesets in set." msgstr "" msgid "First parent for all changesets in set, or the working directory." msgstr "" msgid "Second parent for all changesets in set, or the working directory." msgstr "" msgid "Changesets with no parent changeset in set." msgstr "" msgid "" "An empty set, if any revision in set isn't found; otherwise, all revisions " "in set." msgstr "" msgid "Changeset with lowest revision number in set." msgstr "" msgid "Changeset with highest revision number in set." msgstr "" msgid "First n members of a set." msgstr "" msgid "" "Sort set by keys. The default sort order is ascending, specify a key as \"-" "key\" to sort in descending order." msgstr "" msgid "An alias for \"::.\" (ancestors of the working copy's first parent)." msgstr "" msgid "All changesets, the same as 0:tip." msgstr "" msgid "Revision Set Query" msgstr "" msgid "all revisions converted from subversion" msgstr "" msgid "changeset which represents converted svn revision" msgstr "" msgid "Common sets" msgstr "" msgid "File pattern sets" msgstr "" msgid "Set Ancestry" msgstr "" msgid "Set Logic" msgstr "" msgid "" "help revsets" msgstr "" msgid "Searching..." msgstr "" msgid "Running" msgstr "" msgid "query" msgstr "" msgid "Parse Error: " msgstr "" msgid "Invalid query: " msgstr "" msgid "" "\n" "Caught keyboard interrupt, aborting.\n" msgstr "" #, python-format msgid "can not read file \"%s\". Ignored.\n" msgstr "" #, python-format msgid "" "thg: command '%s' is ambiguous:\n" " %s\n" msgstr "" #, python-format msgid "thg: unknown command '%s'\n" msgstr "" #, python-format msgid "thg %s: %s\n" msgstr "" #, python-format msgid "thg: %s\n" msgstr "" #, python-format msgid "abort: %s!\n" msgstr "" msgid "invalid arguments" msgstr "" #, python-format msgid "unrecognized profiling format '%s' - Ignored\n" msgstr "" msgid "" "lsprof not available - install from http://codespeak.net/svn/user/arigo/hack/" "misc/lsprof/" msgstr "" msgid "repository root directory or symbolic path name" msgstr "" msgid "enable additional output" msgstr "" msgid "suppress output" msgstr "" msgid "display help and exit" msgstr "" msgid "start debugger" msgstr "" msgid "print command execution profile" msgstr "" msgid "do not fork GUI process" msgstr "" msgid "always fork GUI process" msgstr "" msgid "read file list from file" msgstr "" msgid "read file list from file encoding utf-8" msgstr "" msgid "open a new workbench window" msgstr "" msgid "requires a single filename" msgstr "" msgid "thg about" msgstr "" msgid "thg add [FILE]..." msgstr "" msgid "revision to annotate" msgstr "" msgid "open to line" msgstr "" msgid "initial search pattern" msgstr "" msgid "thg annotate" msgstr "" #, python-format msgid "invalid line number: %s" msgstr "" msgid "revision to archive" msgstr "" msgid "thg archive" msgstr "" msgid "merge with old dirstate parent after backout" msgstr "" msgid "parent to choose when backing out merge" msgstr "" msgid "revision to backout" msgstr "" msgid "thg backout [OPTION]... [[-r] REV]" msgstr "" msgid "thg bisect" msgstr "" msgid "revision" msgstr "" msgid "thg bookmarks [-r REV] [NAME]" msgstr "" msgid "only one new bookmark name allowed" msgstr "" msgid "the clone will include an empty working copy (only a repository)" msgstr "" msgid "revision, tag or branch to check out" msgstr "" msgid "include the specified changeset" msgstr "" msgid "clone only the specified branch" msgstr "" msgid "use pull protocol to copy metadata" msgstr "" msgid "use uncompressed transfer (fast over LAN)" msgstr "" msgid "thg clone [OPTION]... SOURCE [DEST]" msgstr "" msgid "record user as committer" msgstr "" msgid "record datecode as commit date" msgstr "" msgid "thg commit [OPTIONS] [FILE]..." msgstr "" msgid "thg debugbugreport [TEXT]" msgstr "" msgid "thg drag_copy SOURCE... DEST" msgstr "" msgid "thg drag_move SOURCE... DEST" msgstr "" msgid "a revision to send" msgstr "" msgid "thg email [REVS]" msgstr "" msgid "use only one form to specify the revision" msgstr "" msgid "thg forget [FILE]..." msgstr "" msgid "revisions to graft" msgstr "" msgid "thg graft [-r] REV..." msgstr "" msgid "You must provide revisions to graft" msgstr "" msgid "ignore case during search" msgstr "" msgid "thg grep" msgstr "" msgid "thg guess" msgstr "" msgid "thg help [COMMAND]" msgstr "" msgid "global options:" msgstr "" msgid "use \"thg help\" for the full list of commands" msgstr "" msgid "" "use \"thg help\" for the full list of commands or \"thg -v\" for details" msgstr "" #, python-format msgid "use \"thg -v help%s\" to show aliases and global options" msgstr "" #, python-format msgid "use \"thg -v help %s\" to show global options" msgstr "" msgid "" "list of commands:\n" "\n" msgstr "" #, python-format msgid "" "\n" "aliases: %s\n" msgstr "" msgid "(no help text available)" msgstr "" msgid "options:\n" msgstr "" msgid "no commands defined\n" msgstr "" msgid "Thg - TortoiseHg's GUI tools for Mercurial SCM (Hg)\n" msgstr "" msgid "" "basic commands:\n" "\n" msgstr "" #, python-format msgid " (default: %s)" msgstr "" msgid "thg hgignore [FILE]" msgstr "" msgid "import to the patch queue (MQ)" msgstr "" msgid "thg import [OPTION] [SOURCE]..." msgstr "" msgid "thg init [DEST]" msgstr "" msgid "(DEPRECATED)" msgstr "" msgid "thg log [OPTIONS] [FILE]" msgstr "" msgid "revision to display" msgstr "" msgid "thg manifest [-r REV] [FILE]" msgstr "" msgid "revision to merge" msgstr "" msgid "thg merge [[-r] REV]" msgstr "" msgid "a revision to post" msgstr "" msgid "thg postreview [-r] REV..." msgstr "" msgid "no revisions specified" msgstr "" msgid "thg purge" msgstr "" msgid "keep original changesets" msgstr "" msgid "keep original branch names" msgstr "" msgid "rebase from the specified changeset" msgstr "" msgid "rebase onto the specified changeset" msgstr "" msgid "thg rebase -s REV -d REV [--keep]" msgstr "" msgid "Rebase already in progress" msgstr "" msgid "Resuming rebase already in progress" msgstr "" msgid "You must provide source and dest arguments" msgstr "" msgid "thg rejects [FILE]" msgstr "" msgid "You must provide the path to a file" msgstr "" msgid "thg remove [FILE]..." msgstr "" msgid "thg rename SOURCE [DEST]..." msgstr "" msgid "field to give initial focus" msgstr "" msgid "thg repoconfig" msgstr "" msgid "thg resolve" msgstr "" msgid "the revision to show" msgstr "" msgid "thg revdetails [-r REV]" msgstr "" msgid "thg revert [FILE]..." msgstr "" msgid "revision to update" msgstr "" msgid "thg rupdate [[-r] REV]" msgstr "" msgid "name of the hgweb config file (serve more than one repository)" msgstr "" msgid "name of the hgweb config file (DEPRECATED)" msgstr "" msgid "thg serve [--web-conf FILE]" msgstr "" msgid "thg shellconfig" msgstr "" msgid "thg shelve" msgstr "" msgid "sign even if the sigfile is modified" msgstr "" msgid "make the signature local" msgstr "" msgid "the key id to sign with" msgstr "" msgid "do not commit the sigfile after signing" msgstr "" msgid "use as commit message" msgstr "" msgid "thg sign [-f] [-l] [-k KEY] [-m TEXT] [REV]" msgstr "" msgid "Please enable the Gpg extension first." msgstr "" msgid "show files without changes" msgstr "" msgid "show ignored files" msgstr "" msgid "thg status [OPTIONS] [FILE]" msgstr "" msgid "discard uncommitted changes (no backup)" msgstr "" msgid "do not back up stripped revisions" msgstr "" msgid "revision to strip" msgstr "" msgid "thg strip [-f] [-n] [[-r] REV]" msgstr "" msgid "thg sync [PEER]" msgstr "" msgid "replace existing tag" msgstr "" msgid "make the tag local" msgstr "" msgid "revision to tag" msgstr "" msgid "remove a tag" msgstr "" msgid "thg tag [-f] [-l] [-m TEXT] [-r REV] [NAME]" msgstr "" msgid "wait until the second ticks over" msgstr "" msgid "notify the shell for paths given" msgstr "" msgid "remove the status cache" msgstr "" msgid "show the contents of the status cache (no update)" msgstr "" msgid "udpate all repos in current dir" msgstr "" msgid "thg thgstatus [OPTION]" msgstr "" msgid "thg update [-C] [[-r] REV]" msgstr "" msgid "thg userconfig" msgstr "" msgid "changeset to view in diff tool" msgstr "" msgid "revisions to view in diff tool" msgstr "" msgid "bundle file to preview" msgstr "" msgid "launch visual diff tool" msgstr "" msgid "print license" msgstr "" msgid "thg version [OPTION]" msgstr "" #, python-format msgid "TortoiseHg Dialogs (version %s), Mercurial (version %s)\n" msgstr "" msgid "Location:" msgstr "" msgid "Discard remote changes, no backup (-C/--clean)" msgstr "" msgid "Perform a push before updating (-p/--push)" msgstr "" msgid "Allow pushing new branches (--new-branch)" msgstr "" msgid "Force push to remote location (-f/--force)" msgstr "" msgid "Log" msgstr "" msgid "Repositories" msgstr "" #, python-format msgid "Running at %s" msgstr "" msgid "Stopped" msgstr "" msgid "TortoiseHg Web Server" msgstr "" msgid "Web Server" msgstr "" msgid "Port:" msgstr "" msgid "Status:" msgstr "" msgid "Start" msgstr "" msgid "Settings" msgstr "" msgid "" msgstr "" msgid "&True" msgstr "" msgid "&False" msgstr "" msgid "&Unspecified" msgstr "" #, python-format msgid "%dpt" msgstr "" msgid "Bold" msgstr "" msgid "Italic" msgstr "" msgid "Strike" msgstr "" msgid "Underline" msgstr "" msgid "&Set..." msgstr "" msgid "&Clear" msgstr "" #, python-format msgid "Failed to load issue tracker: '%s': %s. " msgstr "" msgid "&Browse..." msgstr "" msgid "UI Language" msgstr "" msgid "Specify your preferred user interface language (restart needed)" msgstr "" msgid "Three-way Merge Tool" msgstr "" msgid "" "Graphical merge program for resolving merge conflicts. If left unspecified, " "Mercurial will use the first applicable tool it finds on your system or use " "its internal merge tool that leaves conflict markers in place. Choose " "internal:merge to force conflict markers, internal:prompt to always select " "local or other, or internal:dump to leave files in the working directory for " "manual merging" msgstr "" msgid "Visual Diff Tool" msgstr "" msgid "" "Specify visual diff tool, as described in the [merge-tools] section of your " "Mercurial configuration files. If left unspecified, TortoiseHg will use the " "selected merge tool. Failing that it uses the first applicable tool it finds." msgstr "" msgid "Visual Editor" msgstr "" msgid "" "Specify visual editor, as described in the [editor-tools] section of your " "Mercurial configuration files. If left unspecified, TortoiseHg will use the " "first applicable tool it finds." msgstr "" msgid "Shell" msgstr "" #, python-format msgid "" "Specify the command to launch your preferred terminal shell application. If " "the value includes the string %(reponame)s, the name of the repository will " "be substituted in place of %(reponame)s. (restart needed)
    Default, " "Windows: cmd.exe /K title %(reponame)s
    Default, OS X: not set
    Default, " "other: xterm -T \"%(reponame)s\"" msgstr "" msgid "Immediate Operations" msgstr "" msgid "" "Space separated list of shell operations you would like to be performed " "immediately, without user interaction. Commands are \"add remove revert " "forget\". Default: None (leave blank)" msgstr "" msgid "Tab Width" msgstr "" msgid "" "Specify the number of spaces that tabs expand to in various TortoiseHg " "windows. Default: 8" msgstr "" msgid "Force Repo Tab" msgstr "" msgid "Always show repo tabs, even for a single repo. Default: False" msgstr "" msgid "Monitor Repo Changes" msgstr "" msgid "" "Specify the target filesystem where TortoiseHg monitors changes. Default: " "always" msgstr "" msgid "Max Diff Size" msgstr "" msgid "" "The maximum size file (in KB) that TortoiseHg will show changes for in the " "changelog, status, and commit windows. A value of zero implies no limit. " "Default: 1024 (1MB)" msgstr "" msgid "Fork GUI" msgstr "" msgid "" "When running from the command line, fork a background process to run " "graphical dialogs. Default: True" msgstr "" msgid "Full Path Title" msgstr "" msgid "" "Show a full directory path of the repository in the dialog title instead of " "just the root directory name. Default: False" msgstr "" msgid "Auto-resolve merges" msgstr "" msgid "" "Indicates whether TortoiseHg should attempt to automatically resolve changes " "from both sides to the same file, and only report merge conflicts when this " "is not possible. When False, all files with changes on both sides of the " "merge will report as conflicting, even if the edits are to different parts " "of the file. In either case, when conflicts occur, the user will be invited " "to review and resolve changes manually. Default: False." msgstr "" msgid "Single Workbench Window" msgstr "" msgid "" "Select whether you want to have a single workbench window. If you disable " "this setting you will get a new workbench window everytime that you use the " "\"Hg Workbench\" command on the explorer context menu. Default: True" msgstr "" msgid "Default widget" msgstr "" msgid "" "Select the initial widget that will be shown when opening a repository. " "Default: revdetails" msgstr "" msgid "" "Select the initial revision that will be selected when opening a " "repository. You can select the \"current\" (i.e. the working directory " "parent), the current \"tip\" or the working directory (\"workingdir\"). " "Default: current" msgstr "" msgid "" "Open new tabs next\n" "to the current tab" msgstr "" msgid "" "Should new tabs be open next to the current tab? If False new tabs will be " "open after the last tab. Default: True" msgstr "" msgid "Author Coloring" msgstr "" msgid "" "Color changesets by author name. If not enabled, the changes are colored " "green for merge, red for non-trivial parents, black for normal. Default: " "False" msgstr "" msgid "Full Authorname" msgstr "" msgid "" "Show full authorname in Logview. If not enabled, only a short part, usually " "name without email is shown. Default: False" msgstr "" msgid "Task Tabs" msgstr "" msgid "" "Show tabs along the side of the bottom half of each repo widget allowing one " "to switch task tabs without using the toolbar. Default: off" msgstr "" msgid "Task Toolbar Order" msgstr "" msgid "" "Specify which task buttons you want to show on the task toolbar and in which " "order.
    Type a list of the task button names. Add separators by putting \"|" "\" between task button names.
    Valid names are: log commit mq sync " "manifest grep and pbranch.
    Default: log commit manifest grep pbranch | " "sync" msgstr "" msgid "Long Summary" msgstr "" msgid "" "If true, concatenate multiple lines of changeset summary until they reach 80 " "characters. Default: False" msgstr "" msgid "Log Batch Size" msgstr "" msgid "" "The number of revisions to read and display in the changelog viewer in a " "single batch. Default: 500" msgstr "" msgid "Dead Branches" msgstr "" msgid "" "Comma separated list of branch names that should be ignored when building a " "list of branch names for a repository. Default: None (leave blank)" msgstr "" msgid "Branch Colors" msgstr "" msgid "" "Space separated list of branch names and colors of the form branch:#XXXXXX. " "Spaces and colons in the branch name must be escaped using a backslash (\\). " "Likewise some other characters can be escaped in this way, e.g. \\u0040 will " "be decoded to the @ character, and \\n to a linefeed. Default: None (leave " "blank)" msgstr "" msgid "Hide Tags" msgstr "" msgid "" "Space separated list of tags that will not be shown.Useful example: Specify " "\"qbase qparent qtip\" to hide the standard tags inserted by the Mercurial " "Queues Extension. Default: None (leave blank)" msgstr "" msgid "Activate Bookmarks" msgstr "" msgid "" "Select when TortoiseHg will show a prompt to activate a bookmark when " "updating to a revision that has one or more bookmarks.

    Default: prompt" msgstr "" msgctxt "config item" msgid "Commit" msgstr "" msgid "Username" msgstr "" msgid "" "Name associated with commits. The common format is:
    Full Name <" "email@example.com>" msgstr "" msgid "Summary Line Length" msgstr "" msgid "" "Suggested length of commit message lines. A red vertical line will mark this " "length. CTRL-E will reflow the current paragraph to the specified line " "length. Default: 80" msgstr "" msgid "Close After Commit" msgstr "" msgid "Close the commit tool after every successful commit. Default: False" msgstr "" msgid "Push After Commit" msgstr "" msgid "" "Attempt to push to specified URL or alias after each successful commit. " "Default: No push" msgstr "" msgid "Auto Commit List" msgstr "" msgid "" "Comma separated list of files that are automatically included in every " "commit. Intended for use only as a repository setting. Default: None (leave " "blank)" msgstr "" msgid "Auto Exclude List" msgstr "" msgid "" "Comma separated list of files that are automatically unchecked when the " "status, and commit dialogs are opened. Default: None (leave blank)" msgstr "" msgid "English Messages" msgstr "" msgid "" "Generate English commit messages even if LANGUAGE or LANG environment " "variables are set to a non-English language. This setting is used by the " "Merge, Tag and Backout dialogs. Default: False" msgstr "" msgid "New Commit Phase" msgstr "" msgid "The phase of new commits. Default: draft" msgstr "" msgid "Secret MQ Patches" msgstr "" msgid "Make MQ patches secret (instead of draft). Default: False" msgstr "" msgid "Monitor working
    directory changes" msgstr "" msgid "" "Select when the working directory status list will be refreshed:
    - " "auto: [default] let TortoiseHg decide when to refresh the " "working directory status list.
    TortoiseHg will refresh the status list " "whenever it performs an action that may potentially modify the working " "directory. This may miss any changes that happen outside of TortoiseHg's " "control;
    - always: in addition to the automatic updates above, " "also refresh the status list whenever the user clicks on the \"working dir " "revision\" or on the \"Commit icon\" on the workbench task bar;
    - " "alwayslocal: same as \"always\" but restricts forced refreshes " "to local repos.
    Default: auto" msgstr "" msgid "Confirm adding unknown files" msgstr "" msgid "" "Determines if TortoiseHg should show a confirmation dialog before adding new " "files in a commit. If True, a confirmation dialog will be showed. If False, " "selected new files will be included in the commit with no confirmation " "dialog. Default: True" msgstr "" msgid "Confirm deleting files" msgstr "" msgid "" "Determines if TortoiseHg should show a confirmation dialog before removing " "files in a commit. If True, a confirmation dialog will be showed. If False, " "selected deleted files will be included in the commit with no confirmation " "dialog. Default: True" msgstr "" msgid "Sync" msgstr "" msgid "After Pull Operation" msgstr "" msgid "" "Operation which is performed directly after a successful pull. update " "equates to pull --update, fetch equates to the fetch extension, rebase " "equates to pull --rebase, updateorrebase equates to pull -u --rebase. " "Default: none" msgstr "" msgid "Default Push" msgstr "" msgid "" "Select the revisions that will be pushed by default, whenever you click the " "Push button.

    Default: all" msgstr "" msgid "Confirm Push" msgstr "" msgid "" "Determines if TortoiseHg should show a confirmation dialog before pushing " "changesets. If False, push will be performed without any confirmation " "dialog. Default: True" msgstr "" msgid "Target Combo" msgstr "" msgid "" "Select if TortoiseHg will show a target combo in the sync toolbar." "

    Default: auto" msgstr "" msgid "SSH Command" msgstr "" msgid "" "Command to use for SSH connections.

    Default: \"ssh\" or \"TortoisePlink." "exe -ssh -2\" (Windows)" msgstr "" msgid "Server" msgstr "" msgid "Behavior:" msgstr "" msgid "'Publishing' repository" msgstr "" msgid "" "Controls draft phase behavior when working as a server. When true, pushed " "changesets are set to public in both client and server and pulled or cloned " "changesets are set to public in the client. Default: True" msgstr "" msgid "Web Server:" msgstr "" msgid "" "Repository name to use in the web interface, and by TortoiseHg as a " "shorthand name. Default is the working directory." msgstr "" msgid "Textual description of the repository's purpose or contents." msgstr "" msgid "Contact" msgstr "" msgid "Name or email address of the person in charge of the repository." msgstr "" msgid "Style" msgstr "" msgid "Which template map style to use" msgstr "" msgid "Archive Formats" msgstr "" msgid "Comma separated list of archive formats allowed for downloading" msgstr "" msgid "Port" msgstr "" msgid "Port to listen on" msgstr "" msgid "Push Requires SSL" msgstr "" msgid "" "Whether to require that inbound pushes be transported over SSL to prevent " "password sniffing." msgstr "" msgid "Stripes" msgstr "" msgid "" "How many lines a \"zebra stripe\" should span in multiline output. Default " "is 1; set to 0 to disable." msgstr "" msgid "Max Files" msgstr "" msgid "Maximum number of files to list per changeset. Default: 10" msgstr "" msgid "Max Changes" msgstr "" msgid "Maximum number of changes to list on the changelog. Default: 10" msgstr "" msgid "Allow Push" msgstr "" msgid "" "Whether to allow pushing to the repository. If empty or not set, push is not " "allowed. If the special value \"*\", any remote user can push, including " "unauthenticated users. Otherwise, the remote user must have been " "authenticated, and the authenticated user name must be present in this list " "(separated by whitespace or \",\"). The contents of the allow_push list are " "examined after the deny_push list." msgstr "" msgid "Deny Push" msgstr "" msgid "" "Whether to deny pushing to the repository. If empty or not set, push is not " "denied. If the special value \"*\", all remote users are denied push. " "Otherwise, unauthenticated users are all denied, and any authenticated user " "name present in this list (separated by whitespace or \",\") is also denied. " "The contents of the deny_push list are examined before the allow_push list." msgstr "" msgid "Encoding" msgstr "" msgid "Character encoding name" msgstr "" msgid "Proxy" msgstr "" msgid "Host" msgstr "" msgid "" "Host name and (optional) port of proxy server, for example \"myproxy:8000\"" msgstr "" msgid "Bypass List" msgstr "" msgid "" "Optional. Comma-separated list of host names that should bypass the proxy" msgstr "" msgid "Optional. User name to authenticate with at the proxy server" msgstr "" msgid "Password" msgstr "" msgid "Optional. Password to authenticate with at the proxy server" msgstr "" msgid "From" msgstr "" msgid "Email address to use in the \"From\" header and for the SMTP envelope" msgstr "" msgid "To" msgstr "" msgid "Comma-separated list of recipient email addresses" msgstr "" msgid "Cc" msgstr "" msgid "Comma-separated list of carbon copy recipient email addresses" msgstr "" msgid "Bcc" msgstr "" msgid "Comma-separated list of blind carbon copy recipient email addresses" msgstr "" msgid "method" msgstr "" msgid "" "Optional. Method to use to send email messages. If value is \"smtp" "\" (default), use SMTP (configured below). Otherwise, use as name of " "program to run that acts like sendmail (takes \"-f\" option for sender, list " "of recipients on command line, message on stdin). Normally, setting this to " "\"sendmail\" or \"/usr/sbin/sendmail\" is enough to use sendmail to send " "messages." msgstr "" msgid "SMTP Host" msgstr "" msgid "Host name of mail server" msgstr "" msgid "SMTP Port" msgstr "" msgid "Port to connect to on mail server. Default: 25" msgstr "" msgid "SMTP TLS" msgstr "" msgid "Connect to mail server using TLS. Default: False" msgstr "" msgid "SMTP Username" msgstr "" msgid "Username to authenticate to mail server with" msgstr "" msgid "SMTP Password" msgstr "" msgid "Password to authenticate to mail server with" msgstr "" msgid "Local Hostname" msgstr "" msgid "Hostname the sender can use to identify itself to the mail server." msgstr "" msgid "Diff and Annotate" msgstr "" msgid "Patch EOL" msgstr "" msgid "" "Normalize file line endings during and after patch to lf or crlf. Strict " "does no normalization. Auto does per-file detection, and is the recommended " "setting. Default: strict" msgstr "" msgid "Git Format" msgstr "" msgid "Use git extended diff header format. Default: False" msgstr "" msgid "MQ Git Format" msgstr "" msgid "" "When set to 'auto', mq will automatically use git patches when required to " "avoid losing changes to file modes, copy records or binary files. If set to " "'keep', mq will obey the [diff] section configuration while preserving " "existing git patches upon qrefresh. If set to 'yes' or 'no', mq will " "override the [diff] section and always generate git or regular patches, " "possibly losing data in the second case. Default: auto" msgstr "" msgid "No Dates" msgstr "" msgid "Do not include modification dates in diff headers. Default: False" msgstr "" msgid "Show Function" msgstr "" msgid "Show which function each change is in. Default: False" msgstr "" msgid "Ignore White Space" msgstr "" msgid "Ignore white space when comparing lines in diff views. Default: False" msgstr "" msgid "Ignore WS Amount" msgstr "" msgid "" "Ignore changes in the amount of white space in diff views. Default: False" msgstr "" msgid "Ignore Blank Lines" msgstr "" msgid "Ignore changes whose lines are all blank in diff views. Default: False" msgstr "" msgid "Annotate:" msgstr "" msgid "" "Ignore white space when comparing lines in the annotate view. Default: False" msgstr "" msgid "" "Ignore changes in the amount of white space in the annotate view. Default: " "False" msgstr "" msgid "" "Ignore changes whose lines are all blank in the annotate view. Default: False" msgstr "" msgid "Fonts" msgstr "" msgid "Message Font" msgstr "" msgid "Font used to display commit messages. Default: monospace 10" msgstr "" msgid "Diff Font" msgstr "" msgid "Font used to display text differences. Default: monospace 10" msgstr "" msgid "List Font" msgstr "" msgid "Font used to display file lists. Default: sans 9" msgstr "" msgid "ChangeLog Font" msgstr "" msgid "Font used to display changelog data. Default: monospace 10" msgstr "" msgid "Output Font" msgstr "" msgid "Font used to display output messages. Default: sans 8" msgstr "" msgid "Extensions" msgstr "" msgid "Tools" msgstr "" msgid "Hooks" msgstr "" msgid "Issue Tracking" msgstr "" msgid "Issue Regex" msgstr "" msgid "Defines the regex to match when picking up issue numbers." msgstr "" msgid "Issue Link" msgstr "" msgid "" "Defines the command to run when an issue number is recognized. You may " "include groups in issue.regex, and corresponding {n} tokens in issue.link " "(where n is a non-negative integer). {0} refers to the entire string matched " "by issue.regex, while {1} refers to the first group and so on. If no {n} " "tokensare found in issue.link, the entire matched string is appended instead." msgstr "" msgid "Inline Tags" msgstr "" msgid "Show tags at start of commit message." msgstr "" msgid "Mandatory Issue Reference" msgstr "" msgid "" "When committing, require that a reference to an issue be specified. If " "enabled, the regex configured in 'Issue Regex' must find a match in the " "commit message." msgstr "" msgid "Issue Tracker Plugin" msgstr "" msgid "" "Configures a COM IBugTraqProvider or IBugTrackProvider2 issue tracking " "plugin." msgstr "" msgid "Configure Issue Tracker" msgstr "" msgid "Configure the selected COM Bug Tracker plugin." msgstr "" msgid "Issue Tracker Trigger" msgstr "" msgid "" "Determines when the issue tracker state will be updated by TortoiseHg. Valid " "settings values are:

    Default: never" msgstr "" msgid "Changeset Link" msgstr "" msgid "" "A \"template string\" that, when set, turns the revision number and short " "hashes that are shown on the revision panels into links.
    The \"template " "string\" uses a \"mercurial template\"-like syntax that currently accepts " "two template expressions:

    For example, in order to link " "to bitbucket commit pages you can set this to:
    https://bitbucket.org/" "tortoisehg/thg/commits/{node|short}" msgstr "" msgid "Path to review board example \"http://demo.reviewboard.org\"" msgstr "" msgid "User name to authenticate with review board" msgstr "" msgid "Password to authenticate with review board" msgstr "" msgid "Server Repository ID" msgstr "" msgid "The default repository id for this repo on the review board server" msgstr "" msgid "Target Groups" msgstr "" msgid "A comma separated list of target groups" msgstr "" msgid "Target People" msgstr "" msgid "A comma separated list of target people" msgstr "" msgid "Kiln Bfiles" msgstr "" msgid "Patterns" msgstr "" msgid "" "Files with names meeting the specified patterns will be automatically added " "as bfiles" msgstr "" msgid "Size" msgstr "" msgid "" "Files of at least the specified size (in megabytes) will be added as bfiles" msgstr "" msgid "System Cache" msgstr "" msgid "" "Path to the directory where a system-wide cache of bfiles will be stored" msgstr "" msgid "Largefiles" msgstr "" msgid "" "Files with names meeting the specified patterns will be automatically added " "as largefiles" msgstr "" msgid "Minimum Size" msgstr "" msgid "" "Files of at least the specified size (in megabytes) will be added as " "largefiles" msgstr "" msgid "User Cache" msgstr "" msgid "Path to the directory where a user's cache of largefiles will be stored" msgstr "" msgid "Projrc" msgstr "" msgid "Require confirmation" msgstr "" msgid "" "When to ask the user to confirm the update of the local \"projrc\" " "configuration file when the remote projrc file changes. Possible values are:" "
    • always: [default] Always show a confirmation prompt " "before updating the local .hg/projrc file.
    • first: Show a " "confirmation dialog when the repository is cloned or when a remote projrc " "file is found for the first time.
    • never: Update the local .hg/" "projrc file automatically, without requiring any user confirmation.
    " msgstr "" msgid "Servers" msgstr "" msgid "" "List of Servers from which \"projrc\" configuration files must be pulled. " "Set it to \"*\" to pull from all servers. Set it to \"default\" to pull from " "the default sync path.Default is pull from NO servers." msgstr "" msgid "Include" msgstr "" msgid "" "List of settings that will be pulled from the project configuration file. " "Default is include NO settings." msgstr "" msgid "Exclude" msgstr "" msgid "" "List of settings that will NOT be pulled from the project configuration " "file. Default is exclude none of the included settings." msgstr "" msgid "Update on incoming" msgstr "" msgid "" "Let the user update the projrc on incoming: